diff --git a/.github/workflows/generate-toolkit-docs.yml b/.github/workflows/generate-toolkit-docs.yml index 4b9d5ea8f..e34997998 100644 --- a/.github/workflows/generate-toolkit-docs.yml +++ b/.github/workflows/generate-toolkit-docs.yml @@ -70,6 +70,7 @@ jobs: --llm-concurrency 15 \ --exclude-file ./remove-toolkits.txt \ --ignore-file ./skip-toolkits.txt \ + --custom-sections ./curation \ --output data/toolkits working-directory: toolkit-docs-generator env: diff --git a/biome.jsonc b/biome.jsonc index 46f808c2a..d3d27b9f4 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -133,6 +133,7 @@ "!node_modules", "!public", "!toolkit-docs-generator/data/toolkits", + "!toolkit-docs-generator/curation", "!scripts", "!agents", "!.vscode", diff --git a/package.json b/package.json index 7baa0100f..8abe37f56 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "swagger-ui-react": "5.32.6", "tailwindcss-animate": "1.0.7", "unist-util-visit": "5.1.0", + "yaml": "2.8.3", "zod": "4.3.6" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b42aeaca..8936629c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,6 +88,9 @@ importers: unist-util-visit: specifier: 5.1.0 version: 5.1.0 + yaml: + specifier: 2.8.3 + version: 2.8.3 zod: specifier: 4.3.6 version: 4.3.6 diff --git a/toolkit-docs-generator/ARCHITECTURE.md b/toolkit-docs-generator/ARCHITECTURE.md index 86654dd08..968cd6f6c 100644 --- a/toolkit-docs-generator/ARCHITECTURE.md +++ b/toolkit-docs-generator/ARCHITECTURE.md @@ -12,7 +12,7 @@ The generator does **not** render HTML. It produces structured JSON and optional 1. Fetch tool definitions from the Engine API or Arcade API. 2. Load toolkit metadata from the design system or mock metadata. -3. Load custom sections from JSON files (optional). +3. Compile hand-authored Markdown and MDX curation (optional). 4. Merge all data into `MergedToolkit` objects. 5. Write a JSON file per toolkit and an `index.json` file. 6. Optionally verify output and compute diffs. @@ -24,7 +24,10 @@ The generator does **not** render HTML. It produces structured JSON and optional - `EngineApiSource` fetches tool metadata from the Engine API. - `ArcadeApiSource` fetches tool metadata from the Arcade API. - `DesignSystemMetadataSource` loads toolkit metadata from `@arcadeai/design-system`. -- `CustomSectionsFileSource` loads custom documentation chunks from a JSON file. +- `MarkdownCurationSource` compiles documentation chunks, import declarations, + and subpages from the configured curation directory. When configured, that + directory is globally authoritative: a missing toolkit directory means the + toolkit has no authored curation. - `CombinedToolkitDataSource` merges tools and metadata into one interface. ### Merger @@ -90,6 +93,7 @@ public, read-only values configured through these Vercel environment variables: ## Key files - `src/sources/engine-api.ts` — tool metadata from Engine API +- `src/sources/markdown-curation.ts` — Markdown and MDX curation compiler - `src/sources/toolkit-data-source.ts` — unified data source - `src/merger/data-merger.ts` — merge pipeline - `src/generator/json-generator.ts` — output writer diff --git a/toolkit-docs-generator/README.md b/toolkit-docs-generator/README.md index 0264ce7fb..223425986 100644 --- a/toolkit-docs-generator/README.md +++ b/toolkit-docs-generator/README.md @@ -232,12 +232,26 @@ deletes it and rebuilds `index.json`. - `--api-source` select `tool-metadata` (default with Engine creds), `list-tools` (only with the explicit flag), or `mock` - `--previous-output` compare against a previous output directory -- `--custom-sections` load curated docs sections +- `--custom-sections` load an authoritative Markdown/MDX curation directory - `--skip-examples`, `--skip-summary` disable LLM steps - `--skip-secret-coherence` disable the stale-reference scan + coverage fill (see the Secret coherence section) - `--llm-editor-provider`, `--llm-editor-model`, `--llm-editor-api-key` configure the secret-coherence editor (Sonnet 4.6 by default) - `--no-verify-output` skip output verification +## Authored curation + +Store authored content below `curation//`. Put injectable sections in +`chunks/*.mdx`, import declarations in `imports/*.mdx`, and rich subpages in +`pages/**/*.mdx`. Each file begins with YAML frontmatter for structured placement +metadata; its body is the Markdown or MDX that readers see. Import files use +`type: import` and contain one ESM import declaration. + +When `--custom-sections` is set, the directory is authoritative for every +toolkit. Removing the final curation file for a toolkit clears that toolkit's +authored prose on the next generation run. Invalid frontmatter, invalid MDX, +unknown tool targets, symlinks, unsafe subpage paths, and leftover JSON curation +fail generation instead of silently falling back to stale generated content. + ## Troubleshooting - **Nothing regenerated**: `--skip-unchanged` exits early when tool definitions did not change. diff --git a/toolkit-docs-generator/curation/airtableapi/imports/001.mdx b/toolkit-docs-generator/curation/airtableapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/airtableapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/arcadeengineapi/chunks/001-secrets.mdx b/toolkit-docs-generator/curation/arcadeengineapi/chunks/001-secrets.mdx new file mode 100644 index 000000000..06249daef --- /dev/null +++ b/toolkit-docs-generator/curation/arcadeengineapi/chunks/001-secrets.mdx @@ -0,0 +1,15 @@ +--- +type: section +location: custom_section +position: after +header: "## Secrets" +--- +## Secrets + +This MCP Server requires the `ARCADE_API_KEY` secret to be configured. Learn how to [configure secrets](/guides/create-tools/tool-basics/create-tool-secrets). + +### Getting your Arcade API Key + +To use the Arcade Engine API MCP Server, you need an Arcade API key. This key authenticates your requests to the Arcade Engine. + +Learn how to create and manage your Arcade API keys in the [API Keys documentation](/get-started/setup/api-keys). diff --git a/toolkit-docs-generator/curation/arcadeengineapi/imports/001.mdx b/toolkit-docs-generator/curation/arcadeengineapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/arcadeengineapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/asana/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/asana/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..e4043b1b0 --- /dev/null +++ b/toolkit-docs-generator/curation/asana/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,6 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Asana MCP Server uses the [Asana auth provider](/references/auth-providers/asana) to connect to users' Asana accounts. diff --git a/toolkit-docs-generator/curation/asanaapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/asanaapi/chunks/001-auth.mdx new file mode 100644 index 000000000..3ca2533a3 --- /dev/null +++ b/toolkit-docs-generator/curation/asanaapi/chunks/001-auth.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The AsanaApi MCP Server uses the Auth Provider with id `arcade-asana` to connect to users' AsanaApi accounts. In order to use the MCP Server, you will need to configure the `arcade-asana` auth provider. +For detailed information on configuring the Asana OAuth provider with Arcade, see the [Asana Auth Provider documentation](/references/auth-providers/asana). diff --git a/toolkit-docs-generator/curation/asanaapi/imports/001.mdx b/toolkit-docs-generator/curation/asanaapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/asanaapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/ashbyapi/imports/001.mdx b/toolkit-docs-generator/curation/ashbyapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/ashbyapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/boxapi/imports/001.mdx b/toolkit-docs-generator/curation/boxapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/boxapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/brightdata/chunks/001-secrets.mdx b/toolkit-docs-generator/curation/brightdata/chunks/001-secrets.mdx new file mode 100644 index 000000000..25ccfc481 --- /dev/null +++ b/toolkit-docs-generator/curation/brightdata/chunks/001-secrets.mdx @@ -0,0 +1,29 @@ +--- +type: section +location: custom_section +position: after +header: "## Secrets" +--- +## Secrets + +This tool requires the following secrets: + +- `BRIGHTDATA_API_KEY` +- `BRIGHTDATA_ZONE` + +### Auth + +The Arcade Bright Data MCP Server uses [Bright Data](https://brightdata.com/) to access proxy networks and web scraping infrastructure. + +**Global Environment Variables:** + +- `BRIGHTDATA_API_KEY`: Your Bright Data API key. You can generate this from your [Bright Data dashboard](https://brightdata.com/cp/zones) under Account Settings → API Access. + +- `BRIGHTDATA_ZONE`: Your Bright Data zone name (e.g., `residential_proxy1`). This is the zone identifier you created in your Bright Data dashboard under Proxies & Scraping Infrastructure → Zones. + +**How to get your credentials:** + +1. **API Key**: Navigate to your [Bright Data Control Panel](https://brightdata.com/cp) → Settings → API Access → Generate API Token +2. **Zone**: Go to Zones section in your dashboard, find your zone name in the format shown in the zone username: `brd-customer-{customer_id}-zone-{zone_name}` + +For more details, see the [Bright Data API Documentation](https://docs.brightdata.com/api-reference). diff --git a/toolkit-docs-generator/curation/calendlyapi/imports/001.mdx b/toolkit-docs-generator/curation/calendlyapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/calendlyapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/clickup/chunks/001-taskpriority.mdx b/toolkit-docs-generator/curation/clickup/chunks/001-taskpriority.mdx new file mode 100644 index 000000000..d0b0265be --- /dev/null +++ b/toolkit-docs-generator/curation/clickup/chunks/001-taskpriority.mdx @@ -0,0 +1,12 @@ +--- +type: section +location: custom_section +position: after +header: "## TaskPriority" +--- +## TaskPriority + +- **URGENT**: `URGENT` +- **HIGH**: `HIGH` +- **NORMAL**: `NORMAL` +- **LOW**: `LOW` diff --git a/toolkit-docs-generator/curation/clickup/chunks/002-taskorderby.mdx b/toolkit-docs-generator/curation/clickup/chunks/002-taskorderby.mdx new file mode 100644 index 000000000..969e17f56 --- /dev/null +++ b/toolkit-docs-generator/curation/clickup/chunks/002-taskorderby.mdx @@ -0,0 +1,11 @@ +--- +type: section +location: custom_section +position: after +header: "## TaskOrderBy" +--- +## TaskOrderBy + +- **CREATED**: `created` +- **UPDATED**: `updated` +- **DUE_DATE**: `due_date` diff --git a/toolkit-docs-generator/curation/clickup/chunks/003-commentresolution.mdx b/toolkit-docs-generator/curation/clickup/chunks/003-commentresolution.mdx new file mode 100644 index 000000000..9c280cedb --- /dev/null +++ b/toolkit-docs-generator/curation/clickup/chunks/003-commentresolution.mdx @@ -0,0 +1,10 @@ +--- +type: section +location: custom_section +position: after +header: "## CommentResolution" +--- +## CommentResolution + +- **SET_AS_RESOLVED**: `resolved` +- **SET_AS_UNRESOLVED**: `unresolved` diff --git a/toolkit-docs-generator/curation/clickupapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/clickupapi/chunks/001-auth.mdx new file mode 100644 index 000000000..34e7d6fa2 --- /dev/null +++ b/toolkit-docs-generator/curation/clickupapi/chunks/001-auth.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The ClickupApi MCP Server uses the Auth Provider with id `arcade-clickup` to connect to users' ClickupApi accounts. In order to use the MCP Server, you will need to configure the `arcade-clickup` auth provider. +For detailed information on configuring the ClickUp OAuth provider with Arcade, see the [ClickUp Auth Provider documentation](/references/auth-providers/clickup). diff --git a/toolkit-docs-generator/curation/clickupapi/imports/001.mdx b/toolkit-docs-generator/curation/clickupapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/clickupapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/confluence/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/confluence/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..e37061f5b --- /dev/null +++ b/toolkit-docs-generator/curation/confluence/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Confluence MCP Server uses the [Atlassian auth provider](/references/auth-providers/atlassian) to connect to users' Atlassian accounts. +--- diff --git a/toolkit-docs-generator/curation/cursoragentsapi/imports/001.mdx b/toolkit-docs-generator/curation/cursoragentsapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/cursoragentsapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/customerioapi/imports/001.mdx b/toolkit-docs-generator/curation/customerioapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/customerioapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/customeriopipelinesapi/imports/001.mdx b/toolkit-docs-generator/curation/customeriopipelinesapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/customeriopipelinesapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/customeriotrackapi/imports/001.mdx b/toolkit-docs-generator/curation/customeriotrackapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/customeriotrackapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/datadogapi/chunks/001-authentication.mdx b/toolkit-docs-generator/curation/datadogapi/chunks/001-authentication.mdx new file mode 100644 index 000000000..708fd20fe --- /dev/null +++ b/toolkit-docs-generator/curation/datadogapi/chunks/001-authentication.mdx @@ -0,0 +1,29 @@ +--- +type: section +location: before_available_tools +position: after +header: "## Authentication" +--- +## Authentication + +The Arcade Datadog API MCP Server requires three environment variables to authenticate with the [Datadog API](https://docs.datadoghq.com/api/latest/): + +- `DATADOG_API_KEY` +- `DATADOG_APPLICATION_KEY` +- `DATADOG_BASE_URL` + +**How to obtain your credentials:** + +1. Log in to your [Datadog dashboard](https://app.datadoghq.com/) +2. Navigate to **Organization Settings** (click your profile icon in the bottom left) +3. Go to **API Keys** → click **New Key** → provide a name and click **Create Key** +4. Go to **Application Keys** → click **New Key** → provide a name and click **Create Key** +5. Determine your **Base URL** based on your Datadog site (check the URL in your browser): + - US1: `api.datadoghq.com` + - US3: `api.us3.datadoghq.com` + - US5: `api.us5.datadoghq.com` + - EU1: `api.datadoghq.eu` + - AP1: `api.ap1.datadoghq.com` + - GOV: `api.ddog-gov.com` + +For more details, see the [Datadog API and Application Keys documentation](https://docs.datadoghq.com/account_management/api-app-keys/). diff --git a/toolkit-docs-generator/curation/datadogapi/imports/001.mdx b/toolkit-docs-generator/curation/datadogapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/datadogapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/dropbox/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/dropbox/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..4cd2dff96 --- /dev/null +++ b/toolkit-docs-generator/curation/dropbox/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,6 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Dropbox MCP Server uses the [Dropbox auth provider](/references/auth-providers/dropbox) to connect to users' Dropbox accounts. diff --git a/toolkit-docs-generator/curation/e2b/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/e2b/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..5f8ce34fc --- /dev/null +++ b/toolkit-docs-generator/curation/e2b/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade E2B MCP Server uses [E2B](https://e2b.dev/) to run code in a sandboxed environment. +**Global Environment Variables:** +- `E2B_API_KEY`: Your [E2B](https://e2b.dev/) API key. diff --git a/toolkit-docs-generator/curation/exaapi/imports/001.mdx b/toolkit-docs-generator/curation/exaapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/exaapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/figma/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/figma/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..3797d68c2 --- /dev/null +++ b/toolkit-docs-generator/curation/figma/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,9 @@ +--- +type: markdown +location: auth +position: after +--- + +The `projects:read` scope is **ONLY available in private Figma OAuth apps**. This scope is required for the navigation tools (`GetTeamProjects` and `GetProjectFiles`). +If you need these navigation tools, you must create a private OAuth app through your Figma organization settings. All other tools work with public OAuth apps. + diff --git a/toolkit-docs-generator/curation/figmaapi/imports/001.mdx b/toolkit-docs-generator/curation/figmaapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/figmaapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/firecrawl/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/firecrawl/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..672ba3b5c --- /dev/null +++ b/toolkit-docs-generator/curation/firecrawl/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Firecrawl MCP Server uses [Firecrawl](https://www.firecrawl.dev/) to scrape, crawl, and map websites. +**Global Environment Variables:** +- `FIRECRAWL_API_KEY`: Your [Firecrawl](https://www.firecrawl.dev/) API key. diff --git a/toolkit-docs-generator/curation/freshserviceapi/imports/001.mdx b/toolkit-docs-generator/curation/freshserviceapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/freshserviceapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/github/chunks/001-description-after-warning.mdx b/toolkit-docs-generator/curation/github/chunks/001-description-after-warning.mdx new file mode 100644 index 000000000..12c87eca7 --- /dev/null +++ b/toolkit-docs-generator/curation/github/chunks/001-description-after-warning.mdx @@ -0,0 +1,12 @@ +--- +type: warning +location: description +position: after +--- + + **Critical**: This MCP Server is built for **GitHub Apps**, not OAuth Apps. + + You **must** create a GitHub App (not an OAuth App) to use this server properly. + + 👉 [Complete GitHub App Setup Guide](/references/auth-providers/github) + diff --git a/toolkit-docs-generator/curation/github/chunks/002-description-after-info.mdx b/toolkit-docs-generator/curation/github/chunks/002-description-after-info.mdx new file mode 100644 index 000000000..1d088ce4a --- /dev/null +++ b/toolkit-docs-generator/curation/github/chunks/002-description-after-info.mdx @@ -0,0 +1,8 @@ +--- +type: info +location: description +position: after +--- + + **Configuration**: On Arcade Cloud, these tools work out of the box. Self-hosted and GitHub Enterprise Server users can set the `GITHUB_SERVER_URL` secret in Arcade Dashboard. See [Secrets Setup](#secrets-setup) below. + diff --git a/toolkit-docs-generator/curation/github/chunks/003-github-enterprise-support.mdx b/toolkit-docs-generator/curation/github/chunks/003-github-enterprise-support.mdx new file mode 100644 index 000000000..96aeeddf2 --- /dev/null +++ b/toolkit-docs-generator/curation/github/chunks/003-github-enterprise-support.mdx @@ -0,0 +1,32 @@ +--- +type: info +location: before_available_tools +position: after +header: "## GitHub Enterprise Support" +--- +## GitHub Enterprise Support + + + This MCP Server fully supports **GitHub Enterprise Server 2.22+** + + +**Default Configuration:** +- If no `GITHUB_SERVER_URL` is configured, the default is `https://api.github.com` (GitHub.com) +- All tools work with GitHub.com out of the box + +**For GitHub Enterprise Server:** + +1. Create your GitHub App on your Enterprise instance (not github.com) +2. Configure the `GITHUB_SERVER_URL` secret in Arcade Dashboard (see [Secrets Setup](#secrets-setup) below) +3. Use your Enterprise server's API endpoint + +**Example Enterprise Server URLs:** +- `https://github.yourcompany.com/api/v3` +- `https://enterprise.yourorg.com/api/v3` +- `https://git.company.internal/api/v3` + + + **Note**: GitHub Enterprise Server uses the `/api/v3` path after the hostname. GitHub.com uses `https://api.github.com` (no `/api/v3` suffix). + + +--- diff --git a/toolkit-docs-generator/curation/github/chunks/004-github-app-permissions-summary.mdx b/toolkit-docs-generator/curation/github/chunks/004-github-app-permissions-summary.mdx new file mode 100644 index 000000000..328d2cc7e --- /dev/null +++ b/toolkit-docs-generator/curation/github/chunks/004-github-app-permissions-summary.mdx @@ -0,0 +1,129 @@ +--- +type: section +location: before_available_tools +position: after +header: "## GitHub App Permissions Summary" +--- +## GitHub App Permissions Summary + +When creating your GitHub App, you'll need to grant specific permissions. Here's a quick reference of which tools require which permissions: + +### Repository Permissions + +| Permission | Level | Required For | +|------------|-------|--------------| +| **Contents** | Read | All repository and pull request tools, getting file contents | +| **Contents** | Write | Creating/updating files, creating branches, merging PRs | +| **Issues** | Read & Write | Issue management, PR assignments, managing labels (Issues) | +| **Pull requests** | Read & Write | Pull request management, reviews, managing labels (PRs) | +| **Metadata** | Read | All tools (automatically granted) | +| **Statuses** | Read | `CheckPullRequestMergeStatus` | + +### Organization Permissions + +| Permission | Level | Required For | +|------------|-------|--------------| +| **Members** | Read | Projects, collaborators, org repos, user search | +| **Projects** | Read & Write | All Projects V2 tools | + +### User Permissions + +| Permission | Level | Required For | +|------------|-------|--------------| +| **Read user profile** | Read | User context tools, review workload | +| **Act on behalf of user** | Enabled | `SetStarred` (starring repositories) | + +### Tools by Permission Requirements + +
+Basic Repository Access (Contents Read + Metadata) + +- `GetRepository` +- `CountStargazers` +- `ListStargazers` +- `ListRepositoryActivities` +- `GetFileContents` + +
+ +
+Repository Write (Contents Write + Metadata) + +- `CreateBranch` +- `CreateOrUpdateFile` +- `UpdateFileLines` + +
+ +
+Issue Management (Contents Read + Issues + Metadata) + +- `CreateIssue` +- `UpdateIssue` +- `GetIssue` +- `ListIssues` +- `CreateIssueComment` +- `ListRepositoryLabels` +- `ManageLabels` (for issues) + +
+ +
+Pull Request Read (Contents + Pull requests Read + Metadata) + +- `ListPullRequests` +- `GetPullRequest` +- `ListPullRequestCommits` +- `ListReviewCommentsOnPullRequest` +- `CheckPullRequestMergeStatus` (+ Statuses) + +
+ +
+Pull Request Write (Contents Read + Pull requests Write + Metadata) + +- `UpdatePullRequest` +- `CreatePullRequest` +- `SubmitPullRequestReview` +- `ManagePullRequest` +- `ManagePullRequestReviewers` +- `CreateReviewComment` +- `CreateReplyForReviewComment` +- `ResolveReviewThread` +- `ManageLabels` (for pull requests) +- `MergePullRequest` (+ Contents Write) + +
+ +
+Organization Tools (Contents + Metadata + Members) + +- `ListOrgRepositories` +- `SearchMyRepos` +- `ListRepositoryCollaborators` +- `AssignPullRequestUser` (+ Issues Write) + +
+ +
+Projects V2 (Contents + Metadata + Projects + Members) + +- `ListProjects` +- `ListProjectItems` +- `SearchProjectItem` +- `ListProjectFields` +- `UpdateProjectItem` (Projects Write) + +
+ +
+User Context (Contents + Metadata + Read user profile) + +- `WhoAmI` (+ Members) +- `GetUserRecentActivity` +- `GetUserOpenItems` +- `GetReviewWorkload` (+ Pull requests Read) + +
+ +--- diff --git a/toolkit-docs-generator/curation/github/chunks/005-configuration-setup.mdx b/toolkit-docs-generator/curation/github/chunks/005-configuration-setup.mdx new file mode 100644 index 000000000..ddded305f --- /dev/null +++ b/toolkit-docs-generator/curation/github/chunks/005-configuration-setup.mdx @@ -0,0 +1,55 @@ +--- +type: warning +location: custom_section +position: after +header: "## Configuration & Setup" +--- +## Configuration & Setup + +### Authentication + + + **Critical**: This MCP Server uses **GitHub Apps** authentication, not OAuth Apps. + + You **must** create a GitHub App to use this server. OAuth Apps are not supported. + + +The Arcade GitHub MCP Server uses the [GitHub auth provider](/references/auth-providers/github) to connect to users' GitHub accounts. + +**For Arcade Cloud:** +- No configuration needed +- Your users will see `Arcade` as the requesting application +- All tools work out of the box + +**For Self-Hosted:** +- You must [create your own GitHub App](/references/auth-providers/github#creating-a-github-app) +- [Configure the GitHub auth provider](/references/auth-providers/github#configuring-github-auth-in-arcade) with your app credentials +- Your users will see your application name + + + **New to GitHub Apps?** Read [Why Arcade Uses GitHub Apps](/references/auth-providers/github#why-arcade-uses-github-apps-not-oauth-apps) + to understand the security and compliance benefits. + + +### Secrets Setup + +GitHub tools read an optional `GITHUB_SERVER_URL` secret from the Arcade Dashboard. It defaults to `https://api.github.com`, so you only need to set it for GitHub Enterprise Server. + +**Steps:** + +1. Go to [Arcade Dashboard](https://api.arcade.dev/dashboard) +2. Navigate to **Secrets** in the left sidebar +3. Click **Add Secret** +4. Add the following secrets: + +| Secret Name | Value | Required For | +|-------------|-------|--------------| +| `GITHUB_SERVER_URL` | `https://api.github.com` (default for GitHub.com) | All tools | + + + **Default**: If `GITHUB_SERVER_URL` is not configured, it defaults to `https://api.github.com` (GitHub.com) + + **GitHub Enterprise Users**: Set `GITHUB_SERVER_URL` to your Enterprise server's API endpoint (e.g., `https://github.yourcompany.com/api/v3`). Note that Enterprise uses `/api/v3` path. See [GitHub Enterprise Support](#github-enterprise-support) for details. + + +--- diff --git a/toolkit-docs-generator/curation/github/imports/001.mdx b/toolkit-docs-generator/curation/github/imports/001.mdx new file mode 100644 index 000000000..7fd9ba2fa --- /dev/null +++ b/toolkit-docs-generator/curation/github/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import { Callout, Tabs } from "nextra/components"; diff --git a/toolkit-docs-generator/curation/githubapi/chunks/001-secrets.mdx b/toolkit-docs-generator/curation/githubapi/chunks/001-secrets.mdx new file mode 100644 index 000000000..49bf23808 --- /dev/null +++ b/toolkit-docs-generator/curation/githubapi/chunks/001-secrets.mdx @@ -0,0 +1,11 @@ +--- +type: section +location: custom_section +position: after +header: "## Secrets" +--- +## Secrets + +All tools in this toolset require the following secret: `GIT_SERVER_URL` (learn how to [configure secrets](/guides/create-tools/tool-basics/create-tool-secrets)) + +The `GIT_SERVER_URL` secret specifies the GitHub server URL. Use `https://api.github.com` for regular GitHub.com accounts, or your GitHub Enterprise server URL (e.g., `https://github.your-company.com/api/v3`) for GitHub Enterprise deployments. diff --git a/toolkit-docs-generator/curation/githubapi/chunks/002-auth.mdx b/toolkit-docs-generator/curation/githubapi/chunks/002-auth.mdx new file mode 100644 index 000000000..56ab1bce1 --- /dev/null +++ b/toolkit-docs-generator/curation/githubapi/chunks/002-auth.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The GithubApi MCP Server uses the Auth Provider with id `arcade-github` to connect to users' GithubApi accounts. In order to use the MCP Server, you will need to configure the `arcade-github` auth provider. +For detailed information on configuring the GitHub OAuth provider with Arcade, see the [GitHub Auth Provider documentation](/references/auth-providers/github). diff --git a/toolkit-docs-generator/curation/githubapi/imports/001.mdx b/toolkit-docs-generator/curation/githubapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/githubapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/gmail/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/gmail/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..9ea589dfe --- /dev/null +++ b/toolkit-docs-generator/curation/gmail/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Gmail MCP Server uses the [Google auth provider](/references/auth-providers/google) to connect to users' Google accounts. +--- diff --git a/toolkit-docs-generator/curation/gmail/chunks/002-adding-attachments-to-emails.mdx b/toolkit-docs-generator/curation/gmail/chunks/002-adding-attachments-to-emails.mdx new file mode 100644 index 000000000..81d75c6d1 --- /dev/null +++ b/toolkit-docs-generator/curation/gmail/chunks/002-adding-attachments-to-emails.mdx @@ -0,0 +1,25 @@ +--- +type: markdown +location: after_available_tools +position: after +header: "## Adding attachments to emails" +priority: 10 +--- +## Adding attachments to emails + +The Gmail send, draft, and reply tools take an `attachments` parameter for local files. The agent emits only the file path; a client-side `preToolUse` hook swaps in the bytes before the request leaves your machine, so file contents never enter the model's context window. + +The first time you attach a file, your agent installs the one-time hook for you after you approve. Gmail caps total message size at 25 MB. + +Attachments work on hosts that support a client-side pre-tool hook. On any other host the tool returns a clear error and sends nothing. + +| Host | Status | Notes | +| --- | --- | --- | +| Cursor | Supported | App, plus Cursor cloud and background agents. | +| Claude Code | Supported | v2.0.10+. | +| Codex CLI | Supported | v0.131+. | +| VS Code chat (GitHub Copilot) | Supported | 1.112+, agent mode. | +| Claude Cowork | Documented limitation | Sandboxed to one folder; the hook cannot be installed from inside it. | +| Claude Desktop | Documented limitation | No client-side hook layer. | +| ChatGPT desktop | Documented limitation | No client-side hook surface. | +| Microsoft 365 Copilot | Documented limitation | No host-side rewrite hook. | diff --git a/toolkit-docs-generator/curation/gmail/imports/001.mdx b/toolkit-docs-generator/curation/gmail/imports/001.mdx new file mode 100644 index 000000000..6826321ed --- /dev/null +++ b/toolkit-docs-generator/curation/gmail/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import ScopePicker from "@/app/_components/scope-picker"; diff --git a/toolkit-docs-generator/curation/googlecalendar/chunks/001-auth.mdx b/toolkit-docs-generator/curation/googlecalendar/chunks/001-auth.mdx new file mode 100644 index 000000000..0b932b655 --- /dev/null +++ b/toolkit-docs-generator/curation/googlecalendar/chunks/001-auth.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The Arcade Google Calendar MCP Server uses the [Google auth provider](/references/auth-providers/google) to connect to users' Google accounts. +--- diff --git a/toolkit-docs-generator/curation/googlecalendar/chunks/002-updategooglemeetoptions.mdx b/toolkit-docs-generator/curation/googlecalendar/chunks/002-updategooglemeetoptions.mdx new file mode 100644 index 000000000..166a7ee1b --- /dev/null +++ b/toolkit-docs-generator/curation/googlecalendar/chunks/002-updategooglemeetoptions.mdx @@ -0,0 +1,13 @@ +--- +type: section +location: custom_section +position: after +header: "## UpdateGoogleMeetOptions" +--- +## UpdateGoogleMeetOptions + +- **`NONE`**: No action is taken. +- **`ADD`**: Add the Google Meet link to the event. +- **`REMOVE`**: Remove the Google Meet link from the event. + + diff --git a/toolkit-docs-generator/curation/googlecalendar/imports/001.mdx b/toolkit-docs-generator/curation/googlecalendar/imports/001.mdx new file mode 100644 index 000000000..6826321ed --- /dev/null +++ b/toolkit-docs-generator/curation/googlecalendar/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import ScopePicker from "@/app/_components/scope-picker"; diff --git a/toolkit-docs-generator/curation/googlecontacts/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/googlecontacts/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..6413088c3 --- /dev/null +++ b/toolkit-docs-generator/curation/googlecontacts/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,6 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Google Contacts MCP Server uses the [Google auth provider](/references/auth-providers/google) to connect to users' Google accounts. diff --git a/toolkit-docs-generator/curation/googlecontacts/imports/001.mdx b/toolkit-docs-generator/curation/googlecontacts/imports/001.mdx new file mode 100644 index 000000000..6826321ed --- /dev/null +++ b/toolkit-docs-generator/curation/googlecontacts/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import ScopePicker from "@/app/_components/scope-picker"; diff --git a/toolkit-docs-generator/curation/googledocs/chunks/001-description-after-warning.mdx b/toolkit-docs-generator/curation/googledocs/chunks/001-description-after-warning.mdx new file mode 100644 index 000000000..753a45512 --- /dev/null +++ b/toolkit-docs-generator/curation/googledocs/chunks/001-description-after-warning.mdx @@ -0,0 +1,9 @@ +--- +type: warning +location: description +position: after +--- + + This Toolkit is not available in Arcade Cloud. You can use these tools with a + [self-hosted](/guides/deployment-hosting/configure-engine) instance of Arcade. + diff --git a/toolkit-docs-generator/curation/googledocs/chunks/002-tab-support.mdx b/toolkit-docs-generator/curation/googledocs/chunks/002-tab-support.mdx new file mode 100644 index 000000000..b41e0b5b9 --- /dev/null +++ b/toolkit-docs-generator/curation/googledocs/chunks/002-tab-support.mdx @@ -0,0 +1,20 @@ +--- +type: section +location: custom_section +position: after +header: "## Tab Support" +--- +## Tab Support + +Google Docs supports hierarchical tabs within documents. The Google Docs tools provide comprehensive support for working with tabs: + +- **Tab Metadata**: `GetDocumentMetadata` returns hierarchical tab structures with approximate character and word counts for each tab +- **Tab Content**: `GetDocumentAsDocMD` and `SearchAndRetrieveDocuments` include all tab content in their output +- **Tab Filtering**: `GetDocumentAsDocMD` supports filtering to retrieve content from a specific tab using the `tab_id` parameter + +Tabs are represented with the following structure: +- Each tab has a unique `tabId`, `title`, `index`, and `nestingLevel` +- Tabs can be nested up to 3 levels deep (parent → child → grandchild) +- Tab metadata includes approximate character and word counts for each tab's content + +--- diff --git a/toolkit-docs-generator/curation/googledocs/chunks/003-auth.mdx b/toolkit-docs-generator/curation/googledocs/chunks/003-auth.mdx new file mode 100644 index 000000000..3dacf99eb --- /dev/null +++ b/toolkit-docs-generator/curation/googledocs/chunks/003-auth.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The Arcade Google Docs MCP Server uses the [Google auth provider](/references/auth-providers/google) to connect to users' Google accounts. +--- diff --git a/toolkit-docs-generator/curation/googledocs/imports/001.mdx b/toolkit-docs-generator/curation/googledocs/imports/001.mdx new file mode 100644 index 000000000..6826321ed --- /dev/null +++ b/toolkit-docs-generator/curation/googledocs/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import ScopePicker from "@/app/_components/scope-picker"; diff --git a/toolkit-docs-generator/curation/googledrive/imports/001.mdx b/toolkit-docs-generator/curation/googledrive/imports/001.mdx new file mode 100644 index 000000000..6826321ed --- /dev/null +++ b/toolkit-docs-generator/curation/googledrive/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import ScopePicker from "@/app/_components/scope-picker"; diff --git a/toolkit-docs-generator/curation/googlefinance/chunks/001-auth.mdx b/toolkit-docs-generator/curation/googlefinance/chunks/001-auth.mdx new file mode 100644 index 000000000..666f1fd12 --- /dev/null +++ b/toolkit-docs-generator/curation/googlefinance/chunks/001-auth.mdx @@ -0,0 +1,18 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The Arcade Google Finance MCP Server uses the [SerpAPI](https://serpapi.com/) to get stock data from Google Finance. +- **Secret:** + - `SERP_API_KEY`: Your SerpAPI API key. + + Setting the `SERP_API_KEY` secret is only required if you are + [self-hosting](/guides/deployment-hosting/configure-engine) Arcade. If you're + using Arcade Cloud, the secret is already set for you. To manage your + secrets, go to the [Secrets + page](https://api.arcade.dev/dashboard/auth/secrets) in the Arcade + Dashboard. + +--- diff --git a/toolkit-docs-generator/curation/googlefinance/chunks/002-googlefinancewindow.mdx b/toolkit-docs-generator/curation/googlefinance/chunks/002-googlefinancewindow.mdx new file mode 100644 index 000000000..11154a60a --- /dev/null +++ b/toolkit-docs-generator/curation/googlefinance/chunks/002-googlefinancewindow.mdx @@ -0,0 +1,20 @@ +--- +type: section +location: custom_section +position: after +header: "## GoogleFinanceWindow" +--- +## GoogleFinanceWindow + +Defines the time window for fetching stock data from Google Finance. + +- **`ONE_DAY`**: Represents a 1-day time window. +- **`FIVE_DAYS`**: Represents a 5-day time window. +- **`ONE_MONTH`**: Represents a 1-month time window. +- **`SIX_MONTHS`**: Represents a 6-month time window. +- **`YEAR_TO_DATE`**: Represents the time from the start of the year to the current date. +- **`ONE_YEAR`**: Represents a 1-year time window. +- **`FIVE_YEARS`**: Represents a 5-year time window. +- **`MAX`**: Represents the maximum available time window. + + diff --git a/toolkit-docs-generator/curation/googleflights/chunks/001-auth.mdx b/toolkit-docs-generator/curation/googleflights/chunks/001-auth.mdx new file mode 100644 index 000000000..d34fa8fff --- /dev/null +++ b/toolkit-docs-generator/curation/googleflights/chunks/001-auth.mdx @@ -0,0 +1,17 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The Arcade Google Flights MCP Server uses the [SerpAPI](https://serpapi.com/) to search for flights from Google Flights. +- **Secret:** + - `SERP_API_KEY`: Your SerpAPI API key. + + Setting the `SERP_API_KEY` secret is only required if you are + [self-hosting](/guides/deployment-hosting/configure-engine) Arcade. If you're + using Arcade Cloud, the secret is already set for you. To manage your secrets, + go to the [Secrets page](https://api.arcade.dev/dashboard/auth/secrets) in the + Arcade Dashboard. + +--- diff --git a/toolkit-docs-generator/curation/googleflights/chunks/002-googleflightsmaxstops.mdx b/toolkit-docs-generator/curation/googleflights/chunks/002-googleflightsmaxstops.mdx new file mode 100644 index 000000000..5af6d4996 --- /dev/null +++ b/toolkit-docs-generator/curation/googleflights/chunks/002-googleflightsmaxstops.mdx @@ -0,0 +1,14 @@ +--- +type: section +location: custom_section +position: after +header: "## GoogleFlightsMaxStops" +--- +## GoogleFlightsMaxStops + +Defines the maximum number of stops for flights. + +- **`ANY`**: Any number of stops is allowed. +- **`NONSTOP`**: Only nonstop flights are allowed. +- **`ONE`**: Only flights with one stop are allowed. +- **`TWO`**: Only flights with two stops are allowed. diff --git a/toolkit-docs-generator/curation/googleflights/chunks/003-googleflightssortby.mdx b/toolkit-docs-generator/curation/googleflights/chunks/003-googleflightssortby.mdx new file mode 100644 index 000000000..71abfc8b6 --- /dev/null +++ b/toolkit-docs-generator/curation/googleflights/chunks/003-googleflightssortby.mdx @@ -0,0 +1,16 @@ +--- +type: section +location: custom_section +position: after +header: "## GoogleFlightsSortBy" +--- +## GoogleFlightsSortBy + +Defines the sorting options for flight search results. + +- **`TOP_FLIGHTS`**: Sort by the best available flights. +- **`PRICE`**: Sort by the lowest price. +- **`DEPARTURE_TIME`**: Sort by the earliest departure time. +- **`ARRIVAL_TIME`**: Sort by the earliest arrival time. +- **`DURATION`**: Sort by the shortest flight duration. +- **`EMISSIONS`**: Sort by the lowest carbon emissions. diff --git a/toolkit-docs-generator/curation/googleflights/chunks/004-googleflightstravelclass.mdx b/toolkit-docs-generator/curation/googleflights/chunks/004-googleflightstravelclass.mdx new file mode 100644 index 000000000..b624fe7ef --- /dev/null +++ b/toolkit-docs-generator/curation/googleflights/chunks/004-googleflightstravelclass.mdx @@ -0,0 +1,16 @@ +--- +type: section +location: custom_section +position: after +header: "## GoogleFlightsTravelClass" +--- +## GoogleFlightsTravelClass + +Defines the travel class options for flights. + +- **`ECONOMY`**: Economy class. +- **`PREMIUM_ECONOMY`**: Premium economy class. +- **`BUSINESS`**: Business class. +- **`FIRST`**: First class. + + diff --git a/toolkit-docs-generator/curation/googlehotels/chunks/001-auth.mdx b/toolkit-docs-generator/curation/googlehotels/chunks/001-auth.mdx new file mode 100644 index 000000000..32adef9af --- /dev/null +++ b/toolkit-docs-generator/curation/googlehotels/chunks/001-auth.mdx @@ -0,0 +1,16 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The Arcade Google Hotels MCP Server uses the [SerpAPI](https://serpapi.com/) to search for hotels from Google Hotels. +- **Secret:** + - `SERP_API_KEY`: Your SerpAPI API key. + + Setting the `SERP_API_KEY` secret is only required if you are + [self-hosting](/guides/deployment-hosting/configure-engine) Arcade. If you're + using Arcade Cloud, the secret is already set for you. To manage your secrets, + go to the [Secrets page](https://api.arcade.dev/dashboard/auth/secrets) in the + Arcade Dashboard. + diff --git a/toolkit-docs-generator/curation/googlehotels/chunks/002-googlehotelssortby.mdx b/toolkit-docs-generator/curation/googlehotels/chunks/002-googlehotelssortby.mdx new file mode 100644 index 000000000..ff669d711 --- /dev/null +++ b/toolkit-docs-generator/curation/googlehotels/chunks/002-googlehotelssortby.mdx @@ -0,0 +1,16 @@ +--- +type: section +location: custom_section +position: after +header: "## GoogleHotelsSortBy" +--- +## GoogleHotelsSortBy + +Defines the sorting options for hotel search results. + +- **`RELEVANCE`**: Sort by the most relevant results. +- **`LOWEST_PRICE`**: Sort by the lowest price available. +- **`HIGHEST_RATING`**: Sort by the highest customer ratings. +- **`MOST_REVIEWED`**: Sort by the most reviewed hotels. + + diff --git a/toolkit-docs-generator/curation/googlejobs/chunks/001-auth.mdx b/toolkit-docs-generator/curation/googlejobs/chunks/001-auth.mdx new file mode 100644 index 000000000..46c8fa265 --- /dev/null +++ b/toolkit-docs-generator/curation/googlejobs/chunks/001-auth.mdx @@ -0,0 +1,17 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The Arcade Google Jobs MCP Server uses the [SerpAPI](https://serpapi.com/) to get job data from Google Jobs. +- **Secret:** + - `SERP_API_KEY`: Your SerpAPI API key. + + Setting the `SERP_API_KEY` secret is only required if you are + [self-hosting](/guides/deployment-hosting/configure-engine) Arcade. If you're + using Arcade Cloud, the secret is already set for you. To manage your + secrets, go to the [Secrets + page](https://api.arcade.dev/dashboard/auth/secrets) in the Arcade + Dashboard. + diff --git a/toolkit-docs-generator/curation/googlejobs/chunks/002-default-parameters.mdx b/toolkit-docs-generator/curation/googlejobs/chunks/002-default-parameters.mdx new file mode 100644 index 000000000..018462f5c --- /dev/null +++ b/toolkit-docs-generator/curation/googlejobs/chunks/002-default-parameters.mdx @@ -0,0 +1,20 @@ +--- +type: section +location: custom_section +position: after +header: "## Default parameters" +--- +## Default parameters + +Language is configurable through environment variables. When set, they will be used as default for Google Jobs tools. + +Providing a different value as `language` argument in a tool call will override the default value. + +**Language** + +The language code is a 2-character code that determines the language in which the API will search and return news articles. There are two environment variables: + +- `ARCADE_GOOGLE_LANGUAGE`: a default value for all Google search tools. If not set, defaults to 'en' (English). +- `ARCADE_GOOGLE_JOBS_LANGUAGE`: a default value for the jobs search tools. If not set, defaults to `ARCADE_GOOGLE_LANGUAGE`. + +A list of supported language codes can be found [here](#languagecodes). diff --git a/toolkit-docs-generator/curation/googlejobs/chunks/003-languagecodes.mdx b/toolkit-docs-generator/curation/googlejobs/chunks/003-languagecodes.mdx new file mode 100644 index 000000000..f49a43f73 --- /dev/null +++ b/toolkit-docs-generator/curation/googlejobs/chunks/003-languagecodes.mdx @@ -0,0 +1,41 @@ +--- +type: section +location: custom_section +position: after +header: "## LanguageCodes" +--- +## LanguageCodes + +- **`ar`**: Arabic +- **`bn`**: Bengali +- **`da`**: Danish +- **`de`**: German +- **`el`**: Greek +- **`en`**: English +- **`es`**: Spanish +- **`fi`**: Finnish +- **`fr`**: French +- **`hi`**: Hindi +- **`hu`**: Hungarian +- **`id`**: Indonesian +- **`it`**: Italian +- **`ja`**: Japanese +- **`ko`**: Korean +- **`ms`**: Malay +- **`nl`**: Dutch +- **`no`**: Norwegian +- **`pcm`**: Nigerian Pidgin +- **`pl`**: Polish +- **`pt`**: Portuguese +- **`pt-br`**: Portuguese (Brazil) +- **`pt-pt`**: Portuguese (Portugal) +- **`ru`**: Russian +- **`sv`**: Swedish +- **`tl`**: Filipino +- **`tr`**: Turkish +- **`uk`**: Ukrainian +- **`zh`**: Chinese +- **`zh-cn`**: Chinese (Simplified) +- **`zh-tw`**: Chinese (Traditional) + + diff --git a/toolkit-docs-generator/curation/googlemaps/chunks/001-auth.mdx b/toolkit-docs-generator/curation/googlemaps/chunks/001-auth.mdx new file mode 100644 index 000000000..41816b430 --- /dev/null +++ b/toolkit-docs-generator/curation/googlemaps/chunks/001-auth.mdx @@ -0,0 +1,17 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The Arcade Google Maps MCP Server uses the [SerpAPI](https://serpapi.com/) to get directions. +- **Secret:** + - `SERP_API_KEY`: Your SerpAPI API key. + + Setting the `SERP_API_KEY` secret is only required if you are + [self-hosting](/guides/deployment-hosting/configure-engine) Arcade. If you're + using Arcade Cloud, the secret is already set for you. To manage your + secrets, go to the [Secrets + page](https://api.arcade.dev/dashboard/auth/secrets) in the Arcade + Dashboard. + diff --git a/toolkit-docs-generator/curation/googlemaps/chunks/002-default-parameters.mdx b/toolkit-docs-generator/curation/googlemaps/chunks/002-default-parameters.mdx new file mode 100644 index 000000000..404e0f3fc --- /dev/null +++ b/toolkit-docs-generator/curation/googlemaps/chunks/002-default-parameters.mdx @@ -0,0 +1,57 @@ +--- +type: info +location: custom_section +position: after +header: "## Default parameters" +--- +## Default parameters + +Language, Country, Distance Unit, and Travel Mode are configurable through environment variables. When set, they will be used as default for Google Maps tools. + +Providing a different value as `language`, `country`, `distance_unit`, or `travel_mode` argument in a tool call will override the default value. + +**Language** + +The language code is a 2-character code that determines the language in which the API will search and return directions. There are two environment variables: + +- `ARCADE_GOOGLE_LANGUAGE`: a default value for all Google tools. If not set, defaults to 'en' (English). +- `ARCADE_GOOGLE_MAPS_LANGUAGE`: a default value for the Google Maps tools. If not set, defaults to `ARCADE_GOOGLE_LANGUAGE`. + +A list of supported language codes can be found [here](#languagecodes). + +**Country** + +The country code is a 2-character code that determines the country in which the API will search for directions: + +- `ARCADE_GOOGLE_MAPS_COUNTRY`: a default value for the Google Maps tools. If not set, defaults to `None`. + +A list of supported country codes can be found [here](#countrycodes). + +**Distance Unit** + +The distance unit is a string that determines the unit of distance to use in the Google Maps search: + +- `ARCADE_GOOGLE_MAPS_DISTANCE_UNIT`: a default value for the Google Maps tools. If not set, defaults to `GoogleMapsDistanceUnit.KM`. + +A list of supported distance units can be found [here](#googlemapsdistanceunit). + +**Travel Mode** + +The travel mode is a string that determines the mode of travel to use in the Google Maps search: + +- `ARCADE_GOOGLE_MAPS_TRAVEL_MODE`: a default value for the Google Maps tools. If not set, defaults to `GoogleMapsTravelMode.BEST`. + +A list of supported travel modes can be found [here](#googlemapstravelmode). + +- **Secret:** + - `SERP_API_KEY`: Your SerpAPI API key. + + Setting the `SERP_API_KEY` secret is only required if you are + [self-hosting](/guides/deployment-hosting/configure-engine) Arcade. If you're + using Arcade Cloud, the secret is already set for you. To manage your + secrets, go to the [Secrets + page](https://api.arcade.dev/dashboard/auth/secrets) in the Arcade + Dashboard. + + +--- diff --git a/toolkit-docs-generator/curation/googlenews/chunks/001-auth.mdx b/toolkit-docs-generator/curation/googlenews/chunks/001-auth.mdx new file mode 100644 index 000000000..9eaf0ed57 --- /dev/null +++ b/toolkit-docs-generator/curation/googlenews/chunks/001-auth.mdx @@ -0,0 +1,17 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The Arcade Google News MCP Server uses the [SerpAPI](https://serpapi.com/) to get news data from Google News. +- **Secret:** + - `SERP_API_KEY`: Your SerpAPI API key. + + Setting the `SERP_API_KEY` secret is only required if you are + [self-hosting](/guides/deployment-hosting/configure-engine) Arcade. If you're + using Arcade Cloud, the secret is already set for you. To manage your + secrets, go to the [Secrets + page](https://api.arcade.dev/dashboard/auth/secrets) in the Arcade + Dashboard. + diff --git a/toolkit-docs-generator/curation/googlenews/chunks/002-default-parameters.mdx b/toolkit-docs-generator/curation/googlenews/chunks/002-default-parameters.mdx new file mode 100644 index 000000000..61119065a --- /dev/null +++ b/toolkit-docs-generator/curation/googlenews/chunks/002-default-parameters.mdx @@ -0,0 +1,30 @@ +--- +type: section +location: custom_section +position: after +header: "## Default parameters" +--- +## Default parameters + +Language and Country are configurable through environment variables. When set, they will be used as default for Google News tools. + +Providing a different value as `language_code` or `country_code` argument in the tool call will override the default value. + +**Language** + +The language code is a 2-character code that determines the language in which the API will search and return news articles. There are two environment variables: + +- `ARCADE_GOOGLE_LANGUAGE`: a default value for all Google search tools. If not set, defaults to 'en' (English). +- `ARCADE_GOOGLE_NEWS_LANGUAGE`: a default value for the news search tools. If not set, defaults to `ARCADE_GOOGLE_LANGUAGE`. + +A list of supported language codes can be found [here](#languagecodes). + +**Country** + +The country code is a 2-character code that determines the country in which the API will search for news articles. There are two environment variables: + +- `ARCADE_GOOGLE_NEWS_COUNTRY`: a default value for the `SearchNews` tool. If not set, defaults to `None` (search news globally). + +A list of supported country codes can be found [here](#countrycodes). + +--- diff --git a/toolkit-docs-generator/curation/googlenews/chunks/003-languagecodes.mdx b/toolkit-docs-generator/curation/googlenews/chunks/003-languagecodes.mdx new file mode 100644 index 000000000..14f18fe5e --- /dev/null +++ b/toolkit-docs-generator/curation/googlenews/chunks/003-languagecodes.mdx @@ -0,0 +1,39 @@ +--- +type: section +location: custom_section +position: after +header: "## LanguageCodes" +--- +## LanguageCodes + +- **`ar`**: Arabic +- **`bn`**: Bengali +- **`da`**: Danish +- **`de`**: German +- **`el`**: Greek +- **`en`**: English +- **`es`**: Spanish +- **`fi`**: Finnish +- **`fr`**: French +- **`hi`**: Hindi +- **`hu`**: Hungarian +- **`id`**: Indonesian +- **`it`**: Italian +- **`ja`**: Japanese +- **`ko`**: Korean +- **`ms`**: Malay +- **`nl`**: Dutch +- **`no`**: Norwegian +- **`pcm`**: Nigerian Pidgin +- **`pl`**: Polish +- **`pt`**: Portuguese +- **`pt-br`**: Portuguese (Brazil) +- **`pt-pt`**: Portuguese (Portugal) +- **`ru`**: Russian +- **`sv`**: Swedish +- **`tl`**: Filipino +- **`tr`**: Turkish +- **`uk`**: Ukrainian +- **`zh`**: Chinese +- **`zh-cn`**: Chinese (Simplified) +- **`zh-tw`**: Chinese (Traditional) diff --git a/toolkit-docs-generator/curation/googlenews/chunks/004-countrycodes.mdx b/toolkit-docs-generator/curation/googlenews/chunks/004-countrycodes.mdx new file mode 100644 index 000000000..97e3e6627 --- /dev/null +++ b/toolkit-docs-generator/curation/googlenews/chunks/004-countrycodes.mdx @@ -0,0 +1,254 @@ +--- +type: section +location: custom_section +position: after +header: "## CountryCodes" +--- +## CountryCodes + +- **`af`**: Afghanistan +- **`al`**: Albania +- **`dz`**: Algeria +- **`as`**: American Samoa +- **`ad`**: Andorra +- **`ao`**: Angola +- **`ai`**: Anguilla +- **`aq`**: Antarctica +- **`ag`**: Antigua and Barbuda +- **`ar`**: Argentina +- **`am`**: Armenia +- **`aw`**: Aruba +- **`au`**: Australia +- **`at`**: Austria +- **`az`**: Azerbaijan +- **`bs`**: Bahamas +- **`bh`**: Bahrain +- **`bd`**: Bangladesh +- **`bb`**: Barbados +- **`by`**: Belarus +- **`be`**: Belgium +- **`bz`**: Belize +- **`bj`**: Benin +- **`bm`**: Bermuda +- **`bt`**: Bhutan +- **`bo`**: Bolivia +- **`ba`**: Bosnia and Herzegovina +- **`bw`**: Botswana +- **`bv`**: Bouvet Island +- **`br`**: Brazil +- **`io`**: British Indian Ocean Territory +- **`bn`**: Brunei Darussalam +- **`bg`**: Bulgaria +- **`bf`**: Burkina Faso +- **`bi`**: Burundi +- **`kh`**: Cambodia +- **`cm`**: Cameroon +- **`ca`**: Canada +- **`cv`**: Cape Verde +- **`ky`**: Cayman Islands +- **`cf`**: Central African Republic +- **`td`**: Chad +- **`cl`**: Chile +- **`cn`**: China +- **`cx`**: Christmas Island +- **`cc`**: Cocos (Keeling) Islands +- **`co`**: Colombia +- **`km`**: Comoros +- **`cg`**: Congo +- **`cd`**: Congo, the Democratic Republic of the +- **`ck`**: Cook Islands +- **`cr`**: Costa Rica +- **`ci`**: Cote D'ivoire +- **`hr`**: Croatia +- **`cu`**: Cuba +- **`cy`**: Cyprus +- **`cz`**: Czech Republic +- **`dk`**: Denmark +- **`dj`**: Djibouti +- **`dm`**: Dominica +- **`do`**: Dominican Republic +- **`ec`**: Ecuador +- **`eg`**: Egypt +- **`sv`**: El Salvador +- **`gq`**: Equatorial Guinea +- **`er`**: Eritrea +- **`ee`**: Estonia +- **`et`**: Ethiopia +- **`fk`**: Falkland Islands (Malvinas) +- **`fo`**: Faroe Islands +- **`fj`**: Fiji +- **`fi`**: Finland +- **`fr`**: France +- **`gf`**: French Guiana +- **`pf`**: French Polynesia +- **`tf`**: French Southern Territories +- **`ga`**: Gabon +- **`gm`**: Gambia +- **`ge`**: Georgia +- **`de`**: Germany +- **`gh`**: Ghana +- **`gi`**: Gibraltar +- **`gr`**: Greece +- **`gl`**: Greenland +- **`gd`**: Grenada +- **`gp`**: Guadeloupe +- **`gu`**: Guam +- **`gt`**: Guatemala +- **`gg`**: Guernsey +- **`gn`**: Guinea +- **`gw`**: Guinea-Bissau +- **`gy`**: Guyana +- **`ht`**: Haiti +- **`hm`**: Heard Island and Mcdonald Islands +- **`va`**: Holy See (Vatican City State) +- **`hn`**: Honduras +- **`hk`**: Hong Kong +- **`hu`**: Hungary +- **`is`**: Iceland +- **`in`**: India +- **`id`**: Indonesia +- **`ir`**: Iran, Islamic Republic of +- **`iq`**: Iraq +- **`ie`**: Ireland +- **`im`**: Isle of Man +- **`il`**: Israel +- **`it`**: Italy +- **`je`**: Jersey +- **`jm`**: Jamaica +- **`jp`**: Japan +- **`jo`**: Jordan +- **`kz`**: Kazakhstan +- **`ke`**: Kenya +- **`ki`**: Kiribati +- **`kp`**: Korea, Democratic People's Republic of +- **`kr`**: Korea, Republic of +- **`kw`**: Kuwait +- **`kg`**: Kyrgyzstan +- **`la`**: Lao People's Democratic Republic +- **`lv`**: Latvia +- **`lb`**: Lebanon +- **`ls`**: Lesotho +- **`lr`**: Liberia +- **`ly`**: Libyan Arab Jamahiriya +- **`li`**: Liechtenstein +- **`lt`**: Lithuania +- **`lu`**: Luxembourg +- **`mo`**: Macao +- **`mk`**: Macedonia, the Former Yugosalv Republic of +- **`mg`**: Madagascar +- **`mw`**: Malawi +- **`my`**: Malaysia +- **`mv`**: Maldives +- **`ml`**: Mali +- **`mt`**: Malta +- **`mh`**: Marshall Islands +- **`mq`**: Martinique +- **`mr`**: Mauritania +- **`mu`**: Mauritius +- **`yt`**: Mayotte +- **`mx`**: Mexico +- **`fm`**: Micronesia, Federated States of +- **`md`**: Moldova, Republic of +- **`mc`**: Monaco +- **`mn`**: Mongolia +- **`me`**: Montenegro +- **`ms`**: Montserrat +- **`ma`**: Morocco +- **`mz`**: Mozambique +- **`mm`**: Myanmar +- **`na`**: Namibia +- **`nr`**: Nauru +- **`np`**: Nepal +- **`nl`**: Netherlands +- **`an`**: Netherlands Antilles +- **`nc`**: New Caledonia +- **`nz`**: New Zealand +- **`ni`**: Nicaragua +- **`ne`**: Niger +- **`ng`**: Nigeria +- **`nu`**: Niue +- **`nf`**: Norfolk Island +- **`mp`**: Northern Mariana Islands +- **`no`**: Norway +- **`om`**: Oman +- **`pk`**: Pakistan +- **`pw`**: Palau +- **`ps`**: Palestinian Territory, Occupied +- **`pa`**: Panama +- **`pg`**: Papua New Guinea +- **`py`**: Paraguay +- **`pe`**: Peru +- **`ph`**: Philippines +- **`pn`**: Pitcairn +- **`pl`**: Poland +- **`pt`**: Portugal +- **`pr`**: Puerto Rico +- **`qa`**: Qatar +- **`re`**: Reunion +- **`ro`**: Romania +- **`ru`**: Russian Federation +- **`rw`**: Rwanda +- **`sh`**: Saint Helena +- **`kn`**: Saint Kitts and Nevis +- **`lc`**: Saint Lucia +- **`pm`**: Saint Pierre and Miquelon +- **`vc`**: Saint Vincent and the Grenadines +- **`ws`**: Samoa +- **`sm`**: San Marino +- **`st`**: Sao Tome and Principe +- **`sa`**: Saudi Arabia +- **`sn`**: Senegal +- **`rs`**: Serbia +- **`sc`**: Seychelles +- **`sl`**: Sierra Leone +- **`sg`**: Singapore +- **`sk`**: Slovakia +- **`si`**: Slovenia +- **`sb`**: Solomon Islands +- **`so`**: Somalia +- **`za`**: South Africa +- **`gs`**: South Georgia and the South Sandwich Islands +- **`es`**: Spain +- **`lk`**: Sri Lanka +- **`sd`**: Sudan +- **`sr`**: Suriname +- **`sj`**: Svalbard and Jan Mayen +- **`sz`**: Swaziland +- **`se`**: Sweden +- **`ch`**: Switzerland +- **`sy`**: Syrian Arab Republic +- **`tw`**: Taiwan, Province of China +- **`tj`**: Tajikistan +- **`tz`**: Tanzania, United Republic of +- **`th`**: Thailand +- **`tl`**: Timor-Leste +- **`tg`**: Togo +- **`tk`**: Tokelau +- **`to`**: Tonga +- **`tt`**: Trinidad and Tobago +- **`tn`**: Tunisia +- **`tr`**: Turkiye +- **`tm`**: Turkmenistan +- **`tc`**: Turks and Caicos Islands +- **`tv`**: Tuvalu +- **`ug`**: Uganda +- **`ua`**: Ukraine +- **`ae`**: United Arab Emirates +- **`uk`**: United Kingdom +- **`gb`**: United Kingdom +- **`us`**: United States +- **`um`**: United States Minor Outlying Islands +- **`uy`**: Uruguay +- **`uz`**: Uzbekistan +- **`vu`**: Vanuatu +- **`ve`**: Venezuela +- **`vn`**: Viet Nam +- **`vg`**: Virgin Islands, British +- **`vi`**: Virgin Islands, U.S. +- **`wf`**: Wallis and Futuna +- **`eh`**: Western Sahara +- **`ye`**: Yemen +- **`zm`**: Zambia +- **`zw`**: Zimbabwe + + diff --git a/toolkit-docs-generator/curation/googlesearch/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/googlesearch/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..5dfc63b0c --- /dev/null +++ b/toolkit-docs-generator/curation/googlesearch/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,16 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Google Search MCP Server uses the [SerpAPI](https://serpapi.com/) to get results from a Google search. +- **Secret:** + - `SERP_API_KEY`: Your SerpAPI API key. + + Setting the `SERP_API_KEY` secret is only required if you are + [self-hosting](/guides/deployment-hosting/configure-engine) Arcade. If you're + using Arcade Cloud, the secret is already set for you. To manage your + secrets, go to the [Secrets + page](https://api.arcade.dev/dashboard/auth/secrets) in the Arcade + Dashboard. + diff --git a/toolkit-docs-generator/curation/googlesheets/imports/001.mdx b/toolkit-docs-generator/curation/googlesheets/imports/001.mdx new file mode 100644 index 000000000..6826321ed --- /dev/null +++ b/toolkit-docs-generator/curation/googlesheets/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import ScopePicker from "@/app/_components/scope-picker"; diff --git a/toolkit-docs-generator/curation/googleslides/chunks/001-googleslides-reference.mdx b/toolkit-docs-generator/curation/googleslides/chunks/001-googleslides-reference.mdx new file mode 100644 index 000000000..5fd307a32 --- /dev/null +++ b/toolkit-docs-generator/curation/googleslides/chunks/001-googleslides-reference.mdx @@ -0,0 +1,36 @@ +--- +type: section +location: custom_section +position: after +header: "## GoogleSlides Reference" +--- +## GoogleSlides Reference + +Below is a reference of enumerations used by some tools in the GoogleSlides MCP Server: + +### OrderBy + +- **CREATED_TIME**: `createdTime` +- **CREATED_TIME_DESC**: `createdTime desc` +- **FOLDER**: `folder` +- **FOLDER_DESC**: `folder desc` +- **MODIFIED_BY_ME_TIME**: `modifiedByMeTime` +- **MODIFIED_BY_ME_TIME_DESC**: `modifiedByMeTime desc` +- **MODIFIED_TIME**: `modifiedTime` +- **MODIFIED_TIME_DESC**: `modifiedTime desc` +- **NAME**: `name` +- **NAME_DESC**: `name desc` +- **NAME_NATURAL**: `name_natural` +- **NAME_NATURAL_DESC**: `name_natural desc` +- **QUOTA_BYTES_USED**: `quotaBytesUsed` +- **QUOTA_BYTES_USED_DESC**: `quotaBytesUsed desc` +- **RECENCY**: `recency` +- **RECENCY_DESC**: `recency desc` +- **SHARED_WITH_ME_TIME**: `sharedWithMeTime` +- **SHARED_WITH_ME_TIME_DESC**: `sharedWithMeTime desc` +- **STARRED**: `starred` +- **STARRED_DESC**: `starred desc` +- **VIEWED_BY_ME_TIME**: `viewedByMeTime` +- **VIEWED_BY_ME_TIME_DESC**: `viewedByMeTime desc` + + diff --git a/toolkit-docs-generator/curation/googleslides/imports/001.mdx b/toolkit-docs-generator/curation/googleslides/imports/001.mdx new file mode 100644 index 000000000..6826321ed --- /dev/null +++ b/toolkit-docs-generator/curation/googleslides/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import ScopePicker from "@/app/_components/scope-picker"; diff --git a/toolkit-docs-generator/curation/hubspot/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/hubspot/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..dfbe679f4 --- /dev/null +++ b/toolkit-docs-generator/curation/hubspot/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,6 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Cloud Platform offers a default [Hubspot auth provider](/references/auth-providers/hubspot). If you use it, there's nothing to configure. Your users will see `Arcade` as the name of the application requesting permission. diff --git a/toolkit-docs-generator/curation/hubspotautomationapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/hubspotautomationapi/chunks/001-auth.mdx new file mode 100644 index 000000000..3c776f380 --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotautomationapi/chunks/001-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The HubspotAutomationApi MCP Server uses the Auth Provider with id `arcade-hubspot` to connect to users' HubspotAutomationApi accounts. In order to use the MCP Server, you will need to configure the `arcade-hubspot` auth provider. diff --git a/toolkit-docs-generator/curation/hubspotautomationapi/imports/001.mdx b/toolkit-docs-generator/curation/hubspotautomationapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotautomationapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/hubspotcmsapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/hubspotcmsapi/chunks/001-auth.mdx new file mode 100644 index 000000000..c521822c8 --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotcmsapi/chunks/001-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The HubspotCmsApi MCP Server uses the Auth Provider with id `arcade-hubspot` to connect to users' HubspotCmsApi accounts. In order to use the MCP Server, you will need to configure the `arcade-hubspot` auth provider. diff --git a/toolkit-docs-generator/curation/hubspotcmsapi/imports/001.mdx b/toolkit-docs-generator/curation/hubspotcmsapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotcmsapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/hubspotconversationsapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/hubspotconversationsapi/chunks/001-auth.mdx new file mode 100644 index 000000000..1f74510ca --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotconversationsapi/chunks/001-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The HubspotConversationsApi MCP Server uses the Auth Provider with id `arcade-hubspot` to connect to users' HubspotConversationsApi accounts. In order to use the MCP Server, you will need to configure the `arcade-hubspot` auth provider. diff --git a/toolkit-docs-generator/curation/hubspotconversationsapi/imports/001.mdx b/toolkit-docs-generator/curation/hubspotconversationsapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotconversationsapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/hubspotcrmapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/hubspotcrmapi/chunks/001-auth.mdx new file mode 100644 index 000000000..6febae813 --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotcrmapi/chunks/001-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The HubspotCrmApi MCP Server uses the Auth Provider with id `arcade-hubspot` to connect to users' HubspotCrmApi accounts. In order to use the MCP Server, you will need to configure the `arcade-hubspot` auth provider. diff --git a/toolkit-docs-generator/curation/hubspotcrmapi/imports/001.mdx b/toolkit-docs-generator/curation/hubspotcrmapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotcrmapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/hubspoteventsapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/hubspoteventsapi/chunks/001-auth.mdx new file mode 100644 index 000000000..c0296e264 --- /dev/null +++ b/toolkit-docs-generator/curation/hubspoteventsapi/chunks/001-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The HubspotEventsApi MCP Server uses the Auth Provider with id `arcade-hubspot` to connect to users' HubspotEventsApi accounts. In order to use the MCP Server, you will need to configure the `arcade-hubspot` auth provider. diff --git a/toolkit-docs-generator/curation/hubspoteventsapi/imports/001.mdx b/toolkit-docs-generator/curation/hubspoteventsapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/hubspoteventsapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/hubspotmarketingapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/hubspotmarketingapi/chunks/001-auth.mdx new file mode 100644 index 000000000..e048b5530 --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotmarketingapi/chunks/001-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The HubspotMarketingApi MCP Server uses the Auth Provider with id `arcade-hubspot` to connect to users' HubspotMarketingApi accounts. In order to use the MCP Server, you will need to configure the `arcade-hubspot` auth provider. diff --git a/toolkit-docs-generator/curation/hubspotmarketingapi/imports/001.mdx b/toolkit-docs-generator/curation/hubspotmarketingapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotmarketingapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/hubspotmeetingsapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/hubspotmeetingsapi/chunks/001-auth.mdx new file mode 100644 index 000000000..a006e4136 --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotmeetingsapi/chunks/001-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The HubspotMeetingsApi MCP Server uses the Auth Provider with id `arcade-hubspot` to connect to users' HubspotMeetingsApi accounts. In order to use the MCP Server, you will need to configure the `arcade-hubspot` auth provider. diff --git a/toolkit-docs-generator/curation/hubspotmeetingsapi/imports/001.mdx b/toolkit-docs-generator/curation/hubspotmeetingsapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotmeetingsapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/hubspotusersapi/chunks/001-auth.mdx b/toolkit-docs-generator/curation/hubspotusersapi/chunks/001-auth.mdx new file mode 100644 index 000000000..47295b480 --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotusersapi/chunks/001-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The HubspotUsersApi MCP Server uses the Auth Provider with id `arcade-hubspot` to connect to users' HubspotUsersApi accounts. In order to use the MCP Server, you will need to configure the `arcade-hubspot` auth provider. diff --git a/toolkit-docs-generator/curation/hubspotusersapi/imports/001.mdx b/toolkit-docs-generator/curation/hubspotusersapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/hubspotusersapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/intercomapi/chunks/001-intercom-api-subdomain.mdx b/toolkit-docs-generator/curation/intercomapi/chunks/001-intercom-api-subdomain.mdx new file mode 100644 index 000000000..c4974e50e --- /dev/null +++ b/toolkit-docs-generator/curation/intercomapi/chunks/001-intercom-api-subdomain.mdx @@ -0,0 +1,13 @@ +--- +type: section +location: after_available_tools +position: after +header: "## Intercom API Subdomain" +--- +## Intercom API Subdomain + +The IntercomApi MCP Server requires setting the `INTERCOM_API_SUBDOMAIN` secret in the Arcade Dashboard. The appropriate value depends on the region you are using: + +- For the United States servers, set `INTERCOM_API_SUBDOMAIN` secret to `api` +- For the European servers, set `INTERCOM_API_SUBDOMAIN` secret to `api.eu` +- For the Australian servers, set `INTERCOM_API_SUBDOMAIN` secret to `api.au` diff --git a/toolkit-docs-generator/curation/intercomapi/chunks/002-auth.mdx b/toolkit-docs-generator/curation/intercomapi/chunks/002-auth.mdx new file mode 100644 index 000000000..a407d465b --- /dev/null +++ b/toolkit-docs-generator/curation/intercomapi/chunks/002-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The IntercomApi MCP Server uses the Auth Provider with id `arcade-intercom` to connect to users' IntercomApi accounts. In order to use the MCP Server, you will need to configure the `arcade-intercom` auth provider. diff --git a/toolkit-docs-generator/curation/intercomapi/imports/001.mdx b/toolkit-docs-generator/curation/intercomapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/intercomapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/jira/chunks/001-description-after-warning.mdx b/toolkit-docs-generator/curation/jira/chunks/001-description-after-warning.mdx new file mode 100644 index 000000000..f4677fd0b --- /dev/null +++ b/toolkit-docs-generator/curation/jira/chunks/001-description-after-warning.mdx @@ -0,0 +1,25 @@ +--- +type: warning +location: description +position: after +--- + + +

+ Handling multiple Atlassian Clouds +

+ +A Jira user may have multiple Atlassian Clouds authorized via the same OAuth grant. In such cases, the Jira tools must be called with the `atlassian_cloud_id` argument. The [`Jira.GetAvailableAtlassianClouds`](/resources/integrations/productivity/jira#jiragetavailableatlassianclouds) tool can be used to get the available Atlassian Clouds and their IDs. + +When a tool call does not receive a value for `atlassian_cloud_id` and the user only has a single Atlassian Cloud authorized, the tool will use that. Otherwise, an error will be raised. The error will contain an additional content listing the available Atlassian Clouds and their IDs. + +Your AI Agent or AI-powered chat application can use the tool referenced above (or the exception's additional content) to guide the user into selecting the correct Atlassian Cloud. + +When the user selects an Atlassian Cloud, it may be appropriate to keep this information in the LLM's context window for subsequent tool calls, avoiding the need to ask the user multiple times. + +**_It is the job of the AI Agent or chat application to:_** + +1. Make it clear to the chat's end user which Atlassian Cloud is being used at any moment, to avoid, for example, having a Jira Issue being created in the wrong Atlassian Cloud; +1. Appropriately instruct the LLM and keep the relevant information in its context window, enabling it to correctly call the Jira tools, **especially in multi-turn conversations**. + +
diff --git a/toolkit-docs-generator/curation/jira/pages/environment-variables/page.mdx b/toolkit-docs-generator/curation/jira/pages/environment-variables/page.mdx new file mode 100644 index 000000000..6ddda8cbf --- /dev/null +++ b/toolkit-docs-generator/curation/jira/pages/environment-variables/page.mdx @@ -0,0 +1,37 @@ +--- +type: environment-variables +--- +import { Callout } from "nextra/components"; + +# Jira Environment Variables + +### `JIRA_MAX_CONCURRENT_REQUESTS` + +Arcade uses asynchronous calls to request Jira API endpoints. In some tools, multiple concurrent HTTP requests may be made to speed up execution. This environment variable controls the maximum number of concurrent requests to Jira API in any tool execution. + +The value must be a numeric string with an integer greater than or equal to 1. + +**Default:** `3` + + +### `JIRA_API_REQUEST_TIMEOUT` + +Controls the maximum number of seconds to wait for a response from the Jira API. This is also applied, in some cases, as a global max timeout for multiple requests that are made in a single tool execution. For instance, when a tool needs to paginate results from a given endpoint, this timeout may apply to the entire pagination process in total, not only to the individual requests. + +The value must be a numeric string with an integer greater than or equal to 1. + +**Default:** `30` + + +### `JIRA_CACHE_MAX_ITEMS` + + + The caching strategy does not involve caching Jira API responses that go into tool output, but only internal values. + + +The Arcade Jira MCP Server will cache some values that are repeatedly used in tool execution to enable better performance. This environment variable controls the maximum number of items to hold in each cache. + +The value must be a numeric string with an integer greater than or equal to 1. + +**Default:** `5000` + diff --git a/toolkit-docs-generator/curation/linear/chunks/001-auth.mdx b/toolkit-docs-generator/curation/linear/chunks/001-auth.mdx new file mode 100644 index 000000000..bc89b6f24 --- /dev/null +++ b/toolkit-docs-generator/curation/linear/chunks/001-auth.mdx @@ -0,0 +1,9 @@ +--- +type: markdown +location: custom_section +position: after +header: "## Auth" +--- +## Auth + +The Arcade Linear MCP Server uses the [Linear auth provider](/references/auth-providers/linear) to connect to users' Linear accounts. Please refer to the [Linear auth provider](/references/auth-providers/linear) documentation to learn how to configure auth. diff --git a/toolkit-docs-generator/curation/linkedin/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/linkedin/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..d7d5acf07 --- /dev/null +++ b/toolkit-docs-generator/curation/linkedin/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,6 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade LinkedIn MCP Server uses the [LinkedIn auth provider](/references/auth-providers/linkedin) to connect to users' LinkedIn accounts. diff --git a/toolkit-docs-generator/curation/lumaapi/chunks/001-authentication.mdx b/toolkit-docs-generator/curation/lumaapi/chunks/001-authentication.mdx new file mode 100644 index 000000000..38b7833e7 --- /dev/null +++ b/toolkit-docs-generator/curation/lumaapi/chunks/001-authentication.mdx @@ -0,0 +1,26 @@ +--- +type: warning +location: before_available_tools +position: after +header: "## Authentication" +--- +## Authentication + +The Arcade Luma API MCP Server requires one environment variable to authenticate with the [Luma API](https://docs.luma.com/reference/getting-started-with-your-api): + +- `LUMA_API_KEY` + +**How to obtain your credentials:** + +1. Navigate to your [Luma dashboard](https://lu.ma/) +2. Click on your profile icon and go to **Settings** +3. Navigate to **API** or **Developer Settings** +4. Click **Generate API Key** or **Create New Key** +5. Copy the API key and store it securely + + + The Luma API requires a **Luma Plus** subscription. Be careful with your API + key since it provides full access to your Luma account. + + +For more details, see the [Luma API Getting Started guide](https://docs.luma.com/reference/getting-started-with-your-api). diff --git a/toolkit-docs-generator/curation/lumaapi/imports/001.mdx b/toolkit-docs-generator/curation/lumaapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/lumaapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/mailchimpmarketingapi/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/mailchimpmarketingapi/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..284cb15a7 --- /dev/null +++ b/toolkit-docs-generator/curation/mailchimpmarketingapi/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +--- +The MailchimpMarketingApi MCP Server uses the Auth Provider with id `arcade-mailchimp` to connect to users' MailchimpMarketingApi accounts. In order to use the MCP Server, you will need to configure the `arcade-mailchimp` auth provider. +The Mailchimp OAuth provider enables secure authentication with Mailchimp's Marketing API using OAuth 2.0. This allows your tools and agents to access user data and perform actions on their behalf. For detailed information on setting up the OAuth provider, including how to register your application with Mailchimp and configure the auth provider in Arcade, see the [Mailchimp Auth Provider documentation](/references/auth-providers/mailchimp). diff --git a/toolkit-docs-generator/curation/mailchimpmarketingapi/imports/001.mdx b/toolkit-docs-generator/curation/mailchimpmarketingapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/mailchimpmarketingapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/microsoftteams/chunks/001-description-after-warning.mdx b/toolkit-docs-generator/curation/microsoftteams/chunks/001-description-after-warning.mdx new file mode 100644 index 000000000..ae2e9f7d4 --- /dev/null +++ b/toolkit-docs-generator/curation/microsoftteams/chunks/001-description-after-warning.mdx @@ -0,0 +1,8 @@ +--- +type: warning +location: description +position: after +--- + + The Microsoft Teams MCP Server requires a Microsoft 365 account. Personal Microsoft accounts are not supported. + diff --git a/toolkit-docs-generator/curation/miroapi/imports/001.mdx b/toolkit-docs-generator/curation/miroapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/miroapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/notiontoolkit/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/notiontoolkit/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..2fdec3bc0 --- /dev/null +++ b/toolkit-docs-generator/curation/notiontoolkit/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,6 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Notion MCP Server uses the [Notion auth provider](/references/auth-providers/notion) to connect to users' Notion accounts. diff --git a/toolkit-docs-generator/curation/pagerduty/chunks/001-description-after-warning.mdx b/toolkit-docs-generator/curation/pagerduty/chunks/001-description-after-warning.mdx new file mode 100644 index 000000000..8abaa6798 --- /dev/null +++ b/toolkit-docs-generator/curation/pagerduty/chunks/001-description-after-warning.mdx @@ -0,0 +1,11 @@ +--- +type: warning +location: description +position: after +--- + + Arcade supports Classic PagerDuty apps. Select **read-only** access; all tools + in this MCP Server only read data. (Use read/write only if you add custom + write tools.) See [PagerDuty OAuth + functionality](https://developer.pagerduty.com/docs/oauth-functionality). + diff --git a/toolkit-docs-generator/curation/pagerduty/chunks/002-description-after-info.mdx b/toolkit-docs-generator/curation/pagerduty/chunks/002-description-after-info.mdx new file mode 100644 index 000000000..ad4dde867 --- /dev/null +++ b/toolkit-docs-generator/curation/pagerduty/chunks/002-description-after-info.mdx @@ -0,0 +1,9 @@ +--- +type: info +location: description +position: after +--- + + Configure PagerDuty OAuth in the [PagerDuty auth + provider](/references/auth-providers/pagerduty) before using these tools. + diff --git a/toolkit-docs-generator/curation/pagerduty/chunks/003-auth-after-markdown.mdx b/toolkit-docs-generator/curation/pagerduty/chunks/003-auth-after-markdown.mdx new file mode 100644 index 000000000..c0e1a45bd --- /dev/null +++ b/toolkit-docs-generator/curation/pagerduty/chunks/003-auth-after-markdown.mdx @@ -0,0 +1,10 @@ +--- +type: markdown +location: auth +position: after +--- +PagerDuty requires OAuth2. Configure the PagerDuty auth provider and request the scopes shown above per tool. Tokens are passed as Bearer auth: +``` +Authorization: Bearer +``` +See PagerDuty auth docs: [PagerDuty API Authentication](https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTYz-authentication). diff --git a/toolkit-docs-generator/curation/pagerdutyapi/imports/001.mdx b/toolkit-docs-generator/curation/pagerdutyapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/pagerdutyapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/posthogapi/chunks/001-configuration.mdx b/toolkit-docs-generator/curation/posthogapi/chunks/001-configuration.mdx new file mode 100644 index 000000000..bf6b338a1 --- /dev/null +++ b/toolkit-docs-generator/curation/posthogapi/chunks/001-configuration.mdx @@ -0,0 +1,40 @@ +--- +type: section +location: before_available_tools +position: after +header: "## Configuration" +--- +## Configuration + +**Secrets** + +This tool requires the following secrets: `POSTHOG_SERVER_URL`, `POSTHOG_PERSONAL_API_KEY` (learn how to [configure secrets](/guides/create-tools/tool-basics/create-tool-secrets)) +The PosthogApi MCP Server requires two secrets to authenticate with your PostHog instance: + +### Getting Your PostHog Server URL + +The server URL depends on your PostHog deployment: + +- **PostHog Cloud (US Region)**: `https://us.posthog.com` +- **PostHog Cloud (EU Region)**: `https://eu.posthog.com` +- **Self-Hosted**: Use your instance's base URL (e.g., `https://analytics.yourdomain.com`) + +You can verify your server URL by checking your PostHog account settings or the URL you use to access PostHog. + +### Getting Your Personal API Key + +To generate a PostHog personal API key: + +1. Log in to your PostHog account +2. Click your avatar in the bottom-left corner +3. Select the gear icon to open "Account settings" +4. Navigate to the "Personal API Keys" section +5. Click "+ Create a personal API key" +6. Provide a descriptive label for the key +7. Select the necessary scopes (choose only the scopes required for your use case) +8. Click "Create key" +9. **Copy and securely store the key immediately** - it won't be shown again + +For more details on authentication and API usage, refer to the [PostHog API documentation](https://posthog.com/docs/api). + +Once you have both values, configure them as secrets when using the PosthogApi MCP Server. Learn more about [configuring secrets](/guides/create-tools/tool-basics/create-tool-secrets). diff --git a/toolkit-docs-generator/curation/posthogapi/imports/001.mdx b/toolkit-docs-generator/curation/posthogapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/posthogapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/pylon/chunks/001-description-after-warning.mdx b/toolkit-docs-generator/curation/pylon/chunks/001-description-after-warning.mdx new file mode 100644 index 000000000..27ec8fee2 --- /dev/null +++ b/toolkit-docs-generator/curation/pylon/chunks/001-description-after-warning.mdx @@ -0,0 +1,9 @@ +--- +type: warning +location: description +position: after +--- + + Pylon API tokens are admin-scoped and created in Pylon by an org admin. Store + the token as `PYLON_API_TOKEN` in Arcade secrets. There is no user OAuth. + diff --git a/toolkit-docs-generator/curation/pylon/chunks/002-auth-after-markdown.mdx b/toolkit-docs-generator/curation/pylon/chunks/002-auth-after-markdown.mdx new file mode 100644 index 000000000..eb6b5104f --- /dev/null +++ b/toolkit-docs-generator/curation/pylon/chunks/002-auth-after-markdown.mdx @@ -0,0 +1,16 @@ +--- +type: markdown +location: auth +position: after +--- +Pylon uses Bearer tokens created by an org admin. There is **no OAuth flow**. Generate an API token in the Pylon dashboard and store it as the secret `PYLON_API_TOKEN` in Arcade. All tools require this secret. +**Auth header** +``` +Authorization: Bearer +``` + + Pylon tokens are generated by admins in the Pylon UI and grant org-level + access. Rotate tokens regularly and scope storage to your Arcade project’s + secrets. + +Refer to Pylon’s authentication docs: [Pylon API Authentication](https://docs.usepylon.com/pylon-docs/developer/api/authentication). diff --git a/toolkit-docs-generator/curation/reddit/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/reddit/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..a1988ff20 --- /dev/null +++ b/toolkit-docs-generator/curation/reddit/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,6 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Reddit MCP Server uses the [Reddit auth provider](/references/auth-providers/reddit) to connect to users' Reddit accounts. diff --git a/toolkit-docs-generator/curation/slack/chunks/001-header-after-markdown.mdx b/toolkit-docs-generator/curation/slack/chunks/001-header-after-markdown.mdx new file mode 100644 index 000000000..798a7b983 --- /dev/null +++ b/toolkit-docs-generator/curation/slack/chunks/001-header-after-markdown.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: header +position: after +--- + +Managing channels in Slack requires the `channels:manage` scope, which is only available with bot tokens. Arcade uses user tokens (the type of token Arcade supports), so creating new channels is not possible with this toolkit. You can invite users to an existing channel, but channel creation is not supported. + diff --git a/toolkit-docs-generator/curation/slackapi/imports/001.mdx b/toolkit-docs-generator/curation/slackapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/slackapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/spotify/chunks/001-description-after-warning.mdx b/toolkit-docs-generator/curation/spotify/chunks/001-description-after-warning.mdx new file mode 100644 index 000000000..753a45512 --- /dev/null +++ b/toolkit-docs-generator/curation/spotify/chunks/001-description-after-warning.mdx @@ -0,0 +1,9 @@ +--- +type: warning +location: description +position: after +--- + + This Toolkit is not available in Arcade Cloud. You can use these tools with a + [self-hosted](/guides/deployment-hosting/configure-engine) instance of Arcade. + diff --git a/toolkit-docs-generator/curation/squareupapi/imports/001.mdx b/toolkit-docs-generator/curation/squareupapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/squareupapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/stripe/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/stripe/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..231aee28b --- /dev/null +++ b/toolkit-docs-generator/curation/stripe/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: auth +position: after +--- +The Arcade Stripe MCP Server uses the [Stripe Agent Toolkit](https://github.com/stripe/agent-toolkit) to interact with the Stripe API. +- **Required Secret:** + - `STRIPE_SECRET_KEY`: Your Stripe API key. diff --git a/toolkit-docs-generator/curation/stripeapi/imports/001.mdx b/toolkit-docs-generator/curation/stripeapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/stripeapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/ticktickapi/chunks/001-auth-after-markdown.mdx b/toolkit-docs-generator/curation/ticktickapi/chunks/001-auth-after-markdown.mdx new file mode 100644 index 000000000..d7380f90a --- /dev/null +++ b/toolkit-docs-generator/curation/ticktickapi/chunks/001-auth-after-markdown.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +--- +The TicktickApi MCP Server uses the Auth Provider with id `arcade-ticktick` to connect to users' TickTick accounts. In order to use the MCP Server, you will need to configure the `arcade-ticktick` auth provider. +Learn how to configure the TickTick auth provider in the [TickTick auth provider documentation](/references/auth-providers/ticktick). diff --git a/toolkit-docs-generator/curation/ticktickapi/imports/001.mdx b/toolkit-docs-generator/curation/ticktickapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/ticktickapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/trelloapi/chunks/001-authentication.mdx b/toolkit-docs-generator/curation/trelloapi/chunks/001-authentication.mdx new file mode 100644 index 000000000..3686d320a --- /dev/null +++ b/toolkit-docs-generator/curation/trelloapi/chunks/001-authentication.mdx @@ -0,0 +1,27 @@ +--- +type: section +location: custom_section +position: after +header: "## Authentication" +--- +## Authentication + +The Arcade Trello API MCP Server requires two environment variables to authenticate with the Trello API: + +- `TRELLO_API_KEY` +- `TRELLO_API_TOKEN` + +**How to obtain your credentials:** + +1. Log in to your [Trello account](https://trello.com/) +2. Navigate to the [Power-Ups Admin Portal](https://trello.com/power-ups/admin) +3. Click on "New" to create a new Power-Up or select an existing one +4. In your Power-Up settings, go to the **API Key** tab +5. Your **API Key** will be displayed +6. Click on "Token" link to generate a **Token** (this will require authorization) +7. Authorize the token with the required scopes +8. Copy both the API Key and Token for use in your configuration + +Alternatively, you can directly access your API key at: [https://trello.com/app-key](https://trello.com/app-key) + +For more details, see the [Trello API Authentication documentation](https://developer.atlassian.com/cloud/trello/guides/rest-api/authorization/). diff --git a/toolkit-docs-generator/curation/trelloapi/imports/001.mdx b/toolkit-docs-generator/curation/trelloapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/trelloapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/vercelapi/imports/001.mdx b/toolkit-docs-generator/curation/vercelapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/vercelapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/weaviateapi/chunks/001-authentication.mdx b/toolkit-docs-generator/curation/weaviateapi/chunks/001-authentication.mdx new file mode 100644 index 000000000..e4747faa8 --- /dev/null +++ b/toolkit-docs-generator/curation/weaviateapi/chunks/001-authentication.mdx @@ -0,0 +1,25 @@ +--- +type: section +location: before_available_tools +position: after +header: "## Authentication" +--- +## Authentication + +The Arcade Weaviate API MCP Server requires two environment variables to authenticate with your Weaviate instance: + +- `WEAVIATE_API_KEY` +- `WEAVIATE_SERVER_URL` + +**How to obtain your credentials:** + +1. Log in to your [Weaviate Console](https://console.weaviate.cloud/) +2. Select your Weaviate cluster +3. Navigate to **Details** or **API Keys** section +4. Click **Create API Key** or use an existing key +5. Copy your **API Key** +6. Copy your **Cluster URL** (this is your server URL and must include `https://`) + +**Note:** The `WEAVIATE_SERVER_URL` must include the full URL with the `https://` protocol (e.g., `https://your-cluster.weaviate.network`). + +For more details, see the [Weaviate Authentication documentation](https://weaviate.io/developers/weaviate/configuration/authentication). diff --git a/toolkit-docs-generator/curation/weaviateapi/imports/001.mdx b/toolkit-docs-generator/curation/weaviateapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/weaviateapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/xeroapi/imports/001.mdx b/toolkit-docs-generator/curation/xeroapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/xeroapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/zohobooksapi/chunks/001-secrets.mdx b/toolkit-docs-generator/curation/zohobooksapi/chunks/001-secrets.mdx new file mode 100644 index 000000000..fb7936777 --- /dev/null +++ b/toolkit-docs-generator/curation/zohobooksapi/chunks/001-secrets.mdx @@ -0,0 +1,39 @@ +--- +type: section +location: custom_section +position: after +header: "## Secrets" +--- +## Secrets + +This MCP Server requires the `ZOHO_SERVER_URL` secret to be configured. Learn how to [configure secrets](/guides/create-tools/tool-basics/create-tool-secrets). + +### Getting your Zoho Server URL + +The Zoho Server URL is the base URL for your Zoho account's data center. Zoho operates in multiple data centers around the world, and you must use the correct URL for your account. + +Your Zoho Server URL depends on which data center your account is registered in: + +| Data Center | Server URL | +| ----------- | --------------------------- | +| US | `https://books.zoho.com` | +| EU | `https://books.zoho.eu` | +| India | `https://books.zoho.in` | +| Australia | `https://books.zoho.com.au` | +| China | `https://books.zoho.com.cn` | + +To determine which data center your account uses: + +1. Log in to your Zoho Books account +2. Look at the URL in your browser's address bar +3. The domain (`.com`, `.eu`, `.in`, `.com.au`, or `.com.cn`) indicates your data center + +For example, if you access Zoho Books at `https://books.zoho.eu`, your server URL is `https://books.zoho.eu`. + +The server URL is used as the base for all API requests. For example, when retrieving invoices, the full URL would be constructed as: + +``` +{zoho_server_url}/api/v3/invoices?organization_id=... +``` + +Which would become `https://books.zoho.com/api/v3/invoices?organization_id=...` for US accounts. diff --git a/toolkit-docs-generator/curation/zohobooksapi/chunks/002-auth.mdx b/toolkit-docs-generator/curation/zohobooksapi/chunks/002-auth.mdx new file mode 100644 index 000000000..da1da6c50 --- /dev/null +++ b/toolkit-docs-generator/curation/zohobooksapi/chunks/002-auth.mdx @@ -0,0 +1,8 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The ZohoBooksApi MCP Server uses the Auth Provider with id `arcade-zoho` to connect to users' Zoho Books accounts. In order to use the MCP Server, you will need to configure the `arcade-zoho` auth provider. +Learn how to configure the Zoho auth provider in the [Zoho auth provider documentation](/references/auth-providers/zoho). diff --git a/toolkit-docs-generator/curation/zohobooksapi/imports/001.mdx b/toolkit-docs-generator/curation/zohobooksapi/imports/001.mdx new file mode 100644 index 000000000..d325b093d --- /dev/null +++ b/toolkit-docs-generator/curation/zohobooksapi/imports/001.mdx @@ -0,0 +1,4 @@ +--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; diff --git a/toolkit-docs-generator/curation/zoom/chunks/001-auth.mdx b/toolkit-docs-generator/curation/zoom/chunks/001-auth.mdx new file mode 100644 index 000000000..62c9f438f --- /dev/null +++ b/toolkit-docs-generator/curation/zoom/chunks/001-auth.mdx @@ -0,0 +1,7 @@ +--- +type: markdown +location: auth +position: after +header: "## Auth" +--- +The Arcade Zoom MCP Server uses the [Zoom auth provider](/references/auth-providers/zoom) to connect to users' Zoom accounts. diff --git a/toolkit-docs-generator/curation/zoom/pages/install/page.mdx b/toolkit-docs-generator/curation/zoom/pages/install/page.mdx new file mode 100644 index 000000000..c0fc899f6 --- /dev/null +++ b/toolkit-docs-generator/curation/zoom/pages/install/page.mdx @@ -0,0 +1,112 @@ +--- +type: install +--- +# Arcade for Zoom + +import { Steps, Callout } from "nextra/components"; +import { SignupLink } from "@/app/_components/analytics"; +import { ZoomAuthLink } from "./zoom-auth-link"; + +## Integrate Arcade with your Zoom account + +Arcade securely connects your AI agents to APIs, data, code, and other systems via Tools. Our Zoom integration allows Arcade's tools to connect to your Zoom account, helping you manage meetings and gather information more efficiently. + +You can leverage this app in Arcade's Playground when you log in to the Arcade Dashboard, or in your own applications. + +While the Arcade app for Zoom does not directly expose a Large Language Model (LLM) to you, you will likely use Arcade's tools in conjunction with an LLM. When using LLMs, there's always potential to generate inaccurate responses, summaries, or other output. + +Arcade's Zoom app brings Arcade's powerful AI tool-calling capabilities to your meeting management. The Arcade app for Zoom can: + +- List your upcoming meetings within the next 24 hours +- Retrieve meeting invitation details for specific meetings +- Find the participants and/or registrants for a specific meeting +- and more! + +For more details on what tools are available and what scopes they require, see the [Zoom MCP Server documentation](/resources/integrations/social-communication/zoom). + + + The Arcade Zoom app requires an active Arcade account. If you don't have one + yet,{" "} + sign up for free + . + + +## How it works + + + +### Start using Arcade's Zoom tools + +Use Arcade's [tools for Zoom](/resources/integrations/social-communication/zoom) to: + +- List your upcoming meetings +- Get meeting invitation details +- Find meeting participants and registrants +- and more! + +Try leveraging the Arcade Zoom tools in the Arcade Playground by [chatting with an LLM](https://api.arcade.dev/dashboard/playground/chat) asking, "What meetings do I have scheduled today?" or [executing Zoom tools directly](https://api.arcade.dev/dashboard/playground/execute?toolId=ListUpcomingMeetings&toolkits=%5B%5D&authProviders=%5B%5D&secrets=%5B%5D&input=%7B%22user_id%22%3A%22me%22%7D) without interacting with an LLM. + + + When using LLMs with Zoom, responses may sometimes contain inaccuracies. + Always review AI-generated content before taking action. + + + + +## Support and troubleshooting + +If you encounter any issues connecting Arcade to your Zoom account: + +1. Verify you've granted all required permissions during authorization +2. Ensure your Zoom account is active and in good standing +3. Check that you're using a compatible browser (Chrome, Firefox, Safari, or Edge) +4. Clear your browser cache and cookies, then try again + +### Adding the Arcade Zoom app to your Zoom account + +If using the Arcade playground directly did not work, you can try adding the Arcade Zoom app to your Zoom account by clicking the "Connect with Zoom" button below. + + + + + You'll need to have a Zoom account with appropriate permissions to allow + Arcade to access your Zoom data. + + +### Authorize the requested permissions + +When connecting Arcade to your Zoom account, depending on which Arcade tools you'll be using, you'll be asked to authorize specific permissions: + +- **user:read:user** - Allows Arcade to access basic profile information +- **user:read:email** - Enables Arcade to access your email address +- **meeting:read:meetings** - Enables Arcade to list your upcoming meetings +- **meeting:read:invitation** - Enables Arcade to read meeting invitation details + +These permissions ensure Arcade can perform the necessary functions while protecting your privacy and security. + +### Removing the Arcade Zoom app + +To remove the Arcade Zoom app from your Zoom account, you can do so by going to the [Zoom App Marketplace](https://marketplace.zoom.us/user/installed) and uninstalling the app. + +Arcade only stores authentication tokens, not your Zoom data. These tokens become invalid when you uninstall the app and will eventually expire. To remove tokens immediately, delete the Zoom Auth Provider from the [Arcade Dashboard](https://api.arcade.dev/dashboard/auth/oauth). + +## Privacy and security + +Arcade takes the security of your Zoom data seriously: + +- We only request the minimum permissions needed for our tools to function +- Your Zoom credentials are never stored on our servers +- All communication between Arcade and Zoom is encrypted +- You can revoke Arcade's access to your Zoom account at any time through your [Zoom App Marketplace](https://marketplace.zoom.us/user/installed) + +## Next steps + +The Arcade Zoom app is a sample of what Arcade can do with your Zoom account. For your own applications, you might want to [create your own Zoom app](/references/auth-providers/zoom). Creating your own Zoom application will allow you to brand the app, customize the permissions, and more. + +## Need help? + +If you have any questions or need assistance: + +- Check our [Zoom MCP Server documentation](/resources/integrations/social-communication/zoom) +- [Contact our support team](/resources/contact-us) + diff --git a/toolkit-docs-generator/src/cli/generate-flow.ts b/toolkit-docs-generator/src/cli/generate-flow.ts index 0e046f618..aeabff70a 100644 --- a/toolkit-docs-generator/src/cli/generate-flow.ts +++ b/toolkit-docs-generator/src/cli/generate-flow.ts @@ -1,4 +1,48 @@ -import type { ChangeDetectionResult } from "../diff/index"; +import { access } from "fs/promises"; +import { join } from "path"; +import { + type ChangeDetectionResult, + getChangedToolkitIds, +} from "../diff/index"; + +/** + * Resolve an explicit custom-sections path, or use curation/ in the working + * directory when it exists. This keeps regular generation and change checks + * on the same source of truth. + */ +export const resolveCustomSectionsPath = async ( + explicitPath: string | undefined, + workingDir = process.cwd() +): Promise => { + if (explicitPath) { + return explicitPath; + } + + const defaultPath = join(workingDir, "curation"); + try { + await access(defaultPath); + return defaultPath; + } catch { + return; + } +}; + +/** + * Combine API and curation changes using the same case-insensitive toolkit ID + * semantics used by the generation skip set. + */ +export const getCombinedChangedToolkitIds = ( + changeResult: ChangeDetectionResult, + curationChangedToolkitIds: readonly string[] +): string[] => { + const apiChangedIds = getChangedToolkitIds(changeResult).map((id) => + id.toLowerCase() + ); + const curationChangedIds = curationChangedToolkitIds.map((id) => + id.toLowerCase() + ); + return [...new Set([...apiChangedIds, ...curationChangedIds])].sort(); +}; /** * Extract the lowercase toolkit IDs that were removed (present in previous diff --git a/toolkit-docs-generator/src/cli/index.ts b/toolkit-docs-generator/src/cli/index.ts index 64ff979e7..dedac9e17 100644 --- a/toolkit-docs-generator/src/cli/index.ts +++ b/toolkit-docs-generator/src/cli/index.ts @@ -21,6 +21,7 @@ import { formatChangeSummary, formatDetailedChanges, getChangedToolkitIds, + getChangedToolkitIdsFromCustomSections, hasChanges, } from "../diff/index"; import { parsePreviousToolkitForDiff } from "../diff/previous-output"; @@ -42,9 +43,9 @@ import { assertRequireCompleteMetadata, createDataMerger, } from "../merger/data-merger"; -import { createCustomSectionsFileSource } from "../sources/custom-sections-file"; import { createDesignSystemMetadataSource } from "../sources/design-system-metadata"; import { createEmptyCustomSectionsSource } from "../sources/in-memory"; +import { createMarkdownCurationSource } from "../sources/markdown-curation"; import { createMockMetadataSource } from "../sources/mock-metadata"; import { createDesignSystemProviderIdResolver } from "../sources/oauth-provider-resolver"; import { @@ -83,6 +84,8 @@ import { collectRemovedToolkitIds, computeProcessingStats, filterProvidersBySkipIds, + getCombinedChangedToolkitIds, + resolveCustomSectionsPath, } from "./generate-flow"; const program = new Command(); @@ -215,7 +218,8 @@ const filterProvidersByMetadataPresence = async ( }; const buildChangeLogDetails = ( - result: ReturnType + result: ReturnType, + curationChangedToolkitIds: readonly string[] = [] ): string[] => { const changed = getChangedToolkitIds(result); const removed = result.toolkitChanges @@ -241,6 +245,10 @@ const buildChangeLogDetails = ( details.push(`versionOnly=${versionOnly.join(", ")}`); } + if (curationChangedToolkitIds.length > 0) { + details.push(`curationChanged=${curationChangedToolkitIds.join(", ")}`); + } + return details; }; @@ -881,7 +889,10 @@ program .option("--skip-examples", "Skip LLM example generation", false) .option("--skip-summary", "Skip LLM summary generation", false) .option("--no-verify-output", "Skip output verification") - .option("--custom-sections ", "Path to custom sections JSON") + .option( + "--custom-sections ", + "Path to the authoritative Markdown/MDX curation directory (defaults to ./curation when present)" + ) .option( "--resume", "Resume from previous run, skipping already-generated toolkits", @@ -1243,9 +1254,14 @@ program } } - // Custom sections source - const customSectionsSource = options.customSections - ? createCustomSectionsFileSource(options.customSections) + // Custom sections source. When curation/ is present, use it by default + // so manual generation follows the same merge and diff behavior as + // check-changes and the nightly workflow. + const customSectionsPath = await resolveCustomSectionsPath( + options.customSections + ); + const customSectionsSource = customSectionsPath + ? createMarkdownCurationSource(customSectionsPath) : createEmptyCustomSectionsSource(); // Build provider ID resolver from design system OAuth catalogue @@ -1327,6 +1343,14 @@ program currentToolkitDataForDiff, previousToolkits ?? new Map() ); + const changedCustomSectionIds = new Set( + customSectionsPath + ? getChangedToolkitIdsFromCustomSections( + await customSectionsSource.getAllCustomSections(), + previousToolkits ?? new Map() + ).map((id) => id.toLowerCase()) + : [] + ); const compareDurationMs = Date.now() - compareStartedAt; if (options.verbose) { console.log( @@ -1353,7 +1377,10 @@ program } } - if (!hasChanges(detectedChanges)) { + if ( + !hasChanges(detectedChanges) && + changedCustomSectionIds.size === 0 + ) { spinner.succeed( "No changes detected. All toolkits are up to date." ); @@ -1393,7 +1420,9 @@ program } // Get IDs of changed toolkits - const changedIds = getChangedToolkitIds(detectedChanges); + const changedIds = getCombinedChangedToolkitIds(detectedChanges, [ + ...changedCustomSectionIds, + ]); changedToolkitIds = new Set(changedIds.map((id) => id.toLowerCase())); changeResult = detectedChanges; const changedPreview = @@ -1975,7 +2004,10 @@ program .option("--skip-examples", "Skip LLM example generation", false) .option("--skip-summary", "Skip LLM summary generation", false) .option("--no-verify-output", "Skip output verification") - .option("--custom-sections ", "Path to custom sections JSON") + .option( + "--custom-sections ", + "Path to the authoritative Markdown/MDX curation directory (defaults to ./curation when present)" + ) .option( "--resume", "Resume from previous run, skipping already-generated toolkits", @@ -2214,8 +2246,11 @@ program } } - const customSectionsSource = options.customSections - ? createCustomSectionsFileSource(options.customSections) + const customSectionsPath = await resolveCustomSectionsPath( + options.customSections + ); + const customSectionsSource = customSectionsPath + ? createMarkdownCurationSource(customSectionsPath) : createEmptyCustomSectionsSource(); // Build provider ID resolver from design system OAuth catalogue @@ -2689,6 +2724,10 @@ program "--tool-metadata-key ", "Tool metadata API key (or ENGINE_API_KEY env)" ) + .option( + "--custom-sections ", + "Path to the authoritative Markdown/MDX curation directory (defaults to ./curation when present)" + ) .option("--verbose", "Show detailed tool-level changes", false) .option("--json", "Output as JSON", false) .action( @@ -2703,6 +2742,7 @@ program listToolsPageSize?: number; toolMetadataUrl?: string; toolMetadataKey?: string; + customSections?: string; verbose: boolean; json: boolean; // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: legacy CLI flow @@ -2768,7 +2808,25 @@ program currentToolkitDataForDiff, previousToolkits ); + const customSectionsPath = await resolveCustomSectionsPath( + options.customSections + ); + const customSectionsSource = customSectionsPath + ? createMarkdownCurationSource(customSectionsPath) + : createEmptyCustomSectionsSource(); + const curationChangedToolkitIds = customSectionsPath + ? getChangedToolkitIdsFromCustomSections( + await customSectionsSource.getAllCustomSections(), + previousToolkits + ) + : []; const compareDurationMs = Date.now() - compareStartedAt; + const allChangedToolkitIds = getCombinedChangedToolkitIds( + changeResult, + curationChangedToolkitIds + ); + const hasAnyChanges = + hasChanges(changeResult) || curationChangedToolkitIds.length > 0; spinner.stop(); @@ -2787,6 +2845,13 @@ program console.log( chalk.dim(` Compared signatures in ${compareDurationMs}ms`) ); + if (customSectionsPath) { + console.log( + chalk.dim( + ` Compared curation from ${resolve(customSectionsPath)} (${curationChangedToolkitIds.length} prose change(s))` + ) + ); + } if (previousToolkitLoad.stats.failedFiles.length > 0) { console.log( chalk.yellow( @@ -2815,12 +2880,18 @@ program `loadPreviousDurationMs=${loadPreviousDurationMs}`, `compareDurationMs=${compareDurationMs}`, `previousLoadStats=${formatPreviousToolkitLoadStats(previousToolkitLoad.stats)}`, - ...buildChangeLogDetails(changeResult), + ...(customSectionsPath + ? [`customSections=${resolve(customSectionsPath)}`] + : []), + ...buildChangeLogDetails(changeResult, curationChangedToolkitIds), ], }); await appendLogEntry(logPaths.changeLogPath, { title: "changes", - details: buildChangeLogDetails(changeResult), + details: buildChangeLogDetails( + changeResult, + curationChangedToolkitIds + ), }); // Output results @@ -2829,10 +2900,15 @@ program JSON.stringify( { ...changeResult, + curationChangedToolkitIds, + changedToolkitIds: allChangedToolkitIds, diagnostics: { currentToolkitCount: currentToolkitDataForDiff.size, previousToolkitCount: previousToolkits.size, previousLoad: previousToolkitLoad.stats, + customSectionsPath: customSectionsPath + ? resolve(customSectionsPath) + : null, timingMs: { fetch: fetchDurationMs, loadPrevious: loadPreviousDurationMs, @@ -2850,15 +2926,26 @@ program // Summary console.log(chalk.cyan("Summary:")); console.log(` ${formatChangeSummary(changeResult)}`); + if (curationChangedToolkitIds.length > 0) { + console.log( + chalk.cyan("Curation:"), + `${curationChangedToolkitIds.length} toolkit(s) with prose changes` + ); + } console.log(); // Check if there are any changes - if (hasChanges(changeResult)) { - // Show changed toolkits - const changedIds = getChangedToolkitIds(changeResult); + if (hasAnyChanges) { + const apiChangedIds = new Set( + getChangedToolkitIds(changeResult).map((id) => id.toLowerCase()) + ); + const curationOnlyIds = curationChangedToolkitIds.filter( + (id) => !apiChangedIds.has(id) + ); + console.log( chalk.yellow( - `⚠ ${changedIds.length} toolkit(s) need regeneration:\n` + `⚠ ${allChangedToolkitIds.length} toolkit(s) need regeneration:\n` ) ); @@ -2882,6 +2969,10 @@ program console.log(line); } } + + for (const toolkitId of curationOnlyIds) { + console.log(chalk.yellow(`[CURATION] ${toolkitId}`)); + } } else { // Compact view for (const change of changeResult.toolkitChanges) { @@ -2903,6 +2994,12 @@ program ` ${icon}${change.toolkitId} [${change.currentToolCount} tools]${toolChangeSummary}` ); } + + for (const toolkitId of curationOnlyIds) { + console.log( + ` ${chalk.yellow("~ ")}${toolkitId} [curation prose]` + ); + } } console.log(); diff --git a/toolkit-docs-generator/src/diff/custom-sections-diff.ts b/toolkit-docs-generator/src/diff/custom-sections-diff.ts new file mode 100644 index 000000000..dff996bd1 --- /dev/null +++ b/toolkit-docs-generator/src/diff/custom-sections-diff.ts @@ -0,0 +1,72 @@ +import { + getCustomSectionsSourceHash, + stableStringify, +} from "../merger/data-merger"; +import type { CustomSections, MergedToolkit } from "../types/index"; + +const customSectionsFromToolkit = (toolkit: MergedToolkit): CustomSections => ({ + documentationChunks: toolkit.documentationChunks ?? [], + customImports: toolkit.customImports ?? [], + subPages: toolkit.subPages ?? [], + toolChunks: Object.fromEntries( + (toolkit.tools ?? []) + .filter((tool) => tool.documentationChunks?.length) + .map((tool) => [tool.name, tool.documentationChunks]) + ), +}); + +const findPreviousToolkit = ( + toolkitId: string, + previous: ReadonlyMap +): MergedToolkit | undefined => + [...previous.entries()].find( + ([previousId]) => previousId.toLowerCase() === toolkitId.toLowerCase() + )?.[1]; + +const emptyCustomSections = (): CustomSections => ({ + documentationChunks: [], + customImports: [], + subPages: [], + toolChunks: {}, +}); + +/** + * Return toolkit ids whose authoritative curation differs from the prose + * embedded in the previous artifact. Missing current entries compare as + * empty, so deleting the final source file remains observable. + */ +export const getChangedToolkitIdsFromCustomSections = ( + current: Readonly>, + previous: ReadonlyMap +): string[] => { + const ids = new Set([ + ...Object.keys(current).map((id) => id.toLowerCase()), + ...[...previous.keys()].map((id) => id.toLowerCase()), + ]); + const currentById = new Map( + Object.entries(current).map(([id, sections]) => [ + id.toLowerCase(), + sections, + ]) + ); + const changed: string[] = []; + + for (const toolkitId of ids) { + const currentSections = currentById.get(toolkitId) ?? emptyCustomSections(); + const previousToolkit = findPreviousToolkit(toolkitId, previous); + const previousSections = previousToolkit + ? customSectionsFromToolkit(previousToolkit) + : emptyCustomSections(); + + const currentHash = getCustomSectionsSourceHash(currentSections); + const hasChanged = previousToolkit?.curationSourceHash + ? currentHash !== previousToolkit.curationSourceHash + : stableStringify(currentSections) !== stableStringify(previousSections); + + if (hasChanged) { + changed.push(toolkitId.toLowerCase()); + } + } + + return changed.sort(); +}; diff --git a/toolkit-docs-generator/src/diff/index.ts b/toolkit-docs-generator/src/diff/index.ts index 00627d85c..d76b81b86 100644 --- a/toolkit-docs-generator/src/diff/index.ts +++ b/toolkit-docs-generator/src/diff/index.ts @@ -4,6 +4,7 @@ * Exports change detection functionality for comparing API data with previous output. */ +export { getChangedToolkitIdsFromCustomSections } from "./custom-sections-diff"; export { detectSummaryChanges, formatSummaryChangeSummary, diff --git a/toolkit-docs-generator/src/merger/data-merger.ts b/toolkit-docs-generator/src/merger/data-merger.ts index df10931ae..39268d257 100644 --- a/toolkit-docs-generator/src/merger/data-merger.ts +++ b/toolkit-docs-generator/src/merger/data-merger.ts @@ -5,6 +5,7 @@ * into the final MergedToolkit format. */ +import { createHash } from "node:crypto"; import type { ISecretEditGenerator } from "../llm/secret-edit-generator"; import { isApiSuffixedToolkitId, @@ -146,7 +147,7 @@ export const assertRequireCompleteMetadata = ( }; interface MergeToolkitOptions { - previousToolkit?: MergedToolkit; + previousToolkit?: MergedToolkit | undefined; /** Maximum concurrent LLM calls for tool examples (default: 5) */ llmConcurrency?: number; /** Fallback resolver: toolkit ID → OAuth provider ID (design system) */ @@ -217,6 +218,11 @@ export const stableStringify = (value: unknown): string => { return JSON.stringify(value); }; +export const getCustomSectionsSourceHash = ( + customSections: CustomSections +): string => + createHash("sha256").update(stableStringify(customSections)).digest("hex"); + export type ToolSignatureInput = { name: string; qualifiedName: string; @@ -539,15 +545,22 @@ const transformMetadata = ( const getToolDocumentationChunks = ( toolName: string, toolChunks: { [key: string]: DocumentationChunk[] }, - previousTool?: MergedTool + previousTool: MergedTool | undefined, + customSectionsAuthoritative: boolean ): DocumentationChunk[] => { - const fromSource = toolChunks[toolName] ?? []; + const fromSource = toolChunks[toolName]; + + if (customSectionsAuthoritative) { + return fromSource ?? []; + } + const fromPrevious = previousTool?.documentationChunks ?? []; + const sourceItems = fromSource ?? []; // If source has chunks, use source (it's authoritative) // If source is empty but previous has chunks, preserve previous - if (fromSource.length > 0) { - return fromSource; + if (sourceItems.length > 0) { + return sourceItems; } return fromPrevious; }; @@ -648,8 +661,13 @@ const hasToolkitOverviewChunk = (toolkit: MergedToolkit): boolean => const mergeCustomSectionsArrays = ( fromSource: readonly T[] | undefined, - fromPrevious: readonly T[] | undefined + fromPrevious: readonly T[] | undefined, + authoritative: boolean ): T[] => { + if (authoritative) { + return [...(fromSource ?? [])]; + } + const sourceItems = fromSource ?? []; const previousItems = fromPrevious ?? []; @@ -727,6 +745,7 @@ const buildMergedTools = async (options: { failedTools: FailedTool[]; previousToolByQualifiedName: ReadonlyMap; llmConcurrency: number; + customSectionsAuthoritative: boolean; }): Promise => mapWithConcurrency( options.tools, @@ -737,7 +756,8 @@ const buildMergedTools = async (options: { options.toolExampleGenerator, options.warnings, options.failedTools, - options.previousToolByQualifiedName.get(tool.qualifiedName) + options.previousToolByQualifiedName.get(tool.qualifiedName), + options.customSectionsAuthoritative ), options.llmConcurrency ); @@ -752,6 +772,7 @@ const buildMergedToolkit = (options: { customSections: CustomSections | null; previousToolkit: MergedToolkit | undefined; }): MergedToolkit => { + const customSectionsAuthoritative = options.customSections !== null; const mergedMetadata = applyToolkitTypeOverrides( options.toolkitId, options.metadata @@ -773,20 +794,76 @@ const buildMergedToolkit = (options: { tools: options.tools, documentationChunks: mergeCustomSectionsArrays( options.customSections?.documentationChunks, - options.previousToolkit?.documentationChunks + options.previousToolkit?.documentationChunks, + customSectionsAuthoritative ), customImports: mergeCustomSectionsArrays( options.customSections?.customImports, - options.previousToolkit?.customImports + options.previousToolkit?.customImports, + customSectionsAuthoritative ), subPages: mergeCustomSectionsArrays( options.customSections?.subPages, - options.previousToolkit?.subPages + options.previousToolkit?.subPages, + customSectionsAuthoritative ), + ...(options.customSections + ? { + curationSourceHash: getCustomSectionsSourceHash( + options.customSections + ), + } + : {}), generatedAt: new Date().toISOString(), }; }; +const assertKnownToolChunkTargets = ( + toolkitId: string, + tools: readonly Pick[], + customSections: CustomSections | null +): void => { + if (customSections === null) { + return; + } + + const knownToolNames = new Set(tools.map((tool) => tool.name)); + const unknownToolNames = Object.keys(customSections.toolChunks) + .filter((toolName) => !knownToolNames.has(toolName)) + .sort(); + if (unknownToolNames.length > 0) { + throw new Error( + `Curation for ${toolkitId} targets unknown tool(s): ${unknownToolNames.join(", ")}` + ); + } +}; + +/** Overlay current authored curation without disturbing generated enrichment. */ +export const applyCustomSectionsToToolkit = ( + toolkit: MergedToolkit, + customSections: CustomSections | null, + options: { ignoreUnknownToolChunks?: boolean } = {} +): MergedToolkit => { + if (customSections === null) { + return toolkit; + } + if (!options.ignoreUnknownToolChunks) { + assertKnownToolChunkTargets(toolkit.id, toolkit.tools, customSections); + } + + return { + ...toolkit, + documentationChunks: customSections.documentationChunks, + customImports: customSections.customImports, + subPages: customSections.subPages, + curationSourceHash: getCustomSectionsSourceHash(customSections), + tools: toolkit.tools.map((tool) => ({ + ...tool, + documentationChunks: customSections.toolChunks[tool.name] ?? [], + })), + }; +}; + /** * Transform a tool definition into a merged tool */ @@ -796,12 +873,14 @@ const transformTool = async ( toolExampleGenerator: ToolExampleGenerator | undefined, warnings: string[], failedTools: FailedTool[], - previousTool?: MergedTool + previousTool: MergedTool | undefined, + customSectionsAuthoritative: boolean ): Promise => { const documentationChunks = getToolDocumentationChunks( tool.name, toolChunks, - previousTool + previousTool, + customSectionsAuthoritative ); if (previousTool && shouldReuseExample(tool, previousTool)) { @@ -900,6 +979,8 @@ export const mergeToolkit = async ( const warnings: string[] = []; const failedTools: FailedTool[] = []; + assertKnownToolChunkTargets(toolkitId, tools, customSections); + appendMergeWarnings(warnings, toolkitId, tools, metadata); const version = getToolkitVersion(tools); @@ -941,6 +1022,7 @@ export const mergeToolkit = async ( auth = { ...auth, providerId: resolvedProviderId }; } + const customSectionsAuthoritative = customSections !== null; const toolChunks = (customSections?.toolChunks ?? {}) as { [key: string]: DocumentationChunk[]; }; @@ -956,6 +1038,7 @@ export const mergeToolkit = async ( failedTools, previousToolByQualifiedName, llmConcurrency, + customSectionsAuthoritative, }); const toolkit = buildMergedToolkit({ @@ -1056,11 +1139,16 @@ export class DataMerger { private buildMergeErrorResult( toolkitId: string, message: string, - previousToolkit?: MergedToolkit + previousToolkit: MergedToolkit | undefined, + customSections: CustomSections | null ): MergeResult { if (previousToolkit) { return { - toolkit: previousToolkit, + toolkit: applyCustomSectionsToToolkit(previousToolkit, customSections, { + // A previous artifact can legitimately predate a newly curated tool. + // Preserve it while applying the curation that still has a target. + ignoreUnknownToolChunks: true, + }), warnings: [`Error processing toolkit: ${message}`], failedTools: [], error: message, @@ -1107,7 +1195,8 @@ export class DataMerger { private async recoverMissingMetadata( toolkitId: string, - toolkitData: ToolkitData + toolkitData: ToolkitData, + customSections: CustomSections | null ): Promise { if (!this.preserveLastKnownGood || toolkitData.metadata !== null) { return; @@ -1117,7 +1206,8 @@ export class DataMerger { const result = this.buildMergeErrorResult( toolkitId, "missing design-system metadata", - previousToolkit + previousToolkit, + customSections ); if (this.onToolkitComplete && previousToolkit) { await this.onToolkitComplete(result); @@ -1129,17 +1219,29 @@ export class DataMerger { toolkitId: string, toolkitData: ToolkitData ): Promise { + // Curation is configuration. Parse it outside the recoverable merge path + // so invalid source cannot silently preserve stale generated prose. + const customSections = + await this.customSectionsSource.getCustomSections(toolkitId); + // A `tool:` target that matches no tool is an authoring mistake, so it + // belongs outside the recoverable path below: there it would read as an + // upstream failure, and the run would stay green while that toolkit + // silently kept stale data and dropped the mistargeted chunk. A toolkit + // the API returned no tools for is the outage that recovery exists for, + // and it says nothing about whether the curation is correct. + if (toolkitData.tools.length > 0) { + assertKnownToolChunkTargets(toolkitId, toolkitData.tools, customSections); + } try { const recovered = await this.recoverMissingMetadata( toolkitId, - toolkitData + toolkitData, + customSections ); if (recovered) { return recovered; } - const customSections = - await this.customSectionsSource.getCustomSections(toolkitId); const previousToolkit = this.getPreviousToolkit(toolkitId); const result = await mergeToolkit( toolkitId, @@ -1156,7 +1258,11 @@ export class DataMerger { } ); await this.maybeGenerateSummary(result, previousToolkit); - await this.enforceSecretCoherence(result, previousToolkit); + await this.enforceSecretCoherence( + result, + previousToolkit, + customSections !== null + ); // Write immediately if callback provided (incremental mode) if (this.onToolkitComplete) { @@ -1175,7 +1281,8 @@ export class DataMerger { const result = this.buildMergeErrorResult( toolkitId, message, - previousToolkit + previousToolkit, + customSections ); if (this.onToolkitComplete && previousToolkit) { await this.onToolkitComplete(result); @@ -1265,7 +1372,8 @@ export class DataMerger { private async enforceSecretCoherence( result: MergeResult, - previousToolkit?: MergedToolkit + previousToolkit: MergedToolkit | undefined, + customSectionsAuthoritative: boolean ): Promise { if (this.skipSecretCoherence) { // --skip-secret-coherence disables the entire step: no scan, no @@ -1289,7 +1397,11 @@ export class DataMerger { // re-detected against the edited summary. If cleanup accidentally // dropped a passage that incidentally mentioned a current secret, // the fresh scan notices and the editor restores it. - await this.applyStaleRefCleanup(result, issues); + await this.applyStaleRefCleanup( + result, + issues, + customSectionsAuthoritative + ); const postCleanupIssues = detectSecretCoherenceIssues( result.toolkit, previousToolkit @@ -1322,13 +1434,16 @@ export class DataMerger { private async applyStaleRefCleanup( result: MergeResult, - issues: SecretCoherenceIssues + issues: SecretCoherenceIssues, + customSectionsAuthoritative: boolean ): Promise { const editor = this.secretEditGenerator; if (!editor) { return; } - const targets = groupStaleRefsByTarget(issues.staleReferences); + const targets = groupStaleRefsByTarget(issues.staleReferences).filter( + (target) => !customSectionsAuthoritative || target.kind === "summary" + ); if (targets.length === 0) { return; } @@ -1407,15 +1522,17 @@ export class DataMerger { version ); - const recovered = await this.recoverMissingMetadata(toolkitId, toolkitData); + const customSections = + await this.customSectionsSource.getCustomSections(toolkitId); + const recovered = await this.recoverMissingMetadata( + toolkitId, + toolkitData, + customSections + ); if (recovered) { return recovered; } - // Fetch custom sections - const customSections = - await this.customSectionsSource.getCustomSections(toolkitId); - const previousToolkit = this.getPreviousToolkit(toolkitId); const result = await mergeToolkit( toolkitId, @@ -1432,7 +1549,11 @@ export class DataMerger { } ); await this.maybeGenerateSummary(result, previousToolkit); - await this.enforceSecretCoherence(result, previousToolkit); + await this.enforceSecretCoherence( + result, + previousToolkit, + customSections !== null + ); return result; } diff --git a/toolkit-docs-generator/src/shared/toolkit-schemas.ts b/toolkit-docs-generator/src/shared/toolkit-schemas.ts index eb56aab02..42f5a5f66 100644 --- a/toolkit-docs-generator/src/shared/toolkit-schemas.ts +++ b/toolkit-docs-generator/src/shared/toolkit-schemas.ts @@ -389,6 +389,11 @@ export const MergedToolkitSchema = z.object({ * accepted an explicit override here, so it stays part of the contract. */ pipPackageName: z.string().optional(), + /** + * SHA-256 fingerprint of hand-authored curation before generated + * post-processing edits. Used only for incremental generation. + */ + curationSourceHash: z.string().optional(), /** Generation metadata */ generatedAt: z.string().optional(), }); diff --git a/toolkit-docs-generator/src/sources/custom-sections-file.ts b/toolkit-docs-generator/src/sources/custom-sections-file.ts deleted file mode 100644 index 624970f9f..000000000 --- a/toolkit-docs-generator/src/sources/custom-sections-file.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Custom Sections File Source - * - * Loads custom documentation sections from a JSON file. - * This file is produced by the one-time MDX extraction script. - */ -import { access, readFile } from "fs/promises"; -import { z } from "zod"; -import type { CustomSections } from "../types/index"; -import { DocumentationChunkSchema, ToolkitSubPageSchema } from "../types/index"; -import { normalizeId } from "../utils/fp"; -import type { ICustomSectionsSource } from "./interfaces"; - -// ============================================================================ -// File Schema -// ============================================================================ - -const CustomSectionsFileSchema = z.record( - z.string(), - z.object({ - documentationChunks: z.array(DocumentationChunkSchema).default([]), - customImports: z.array(z.string()).default([]), - subPages: z.array(ToolkitSubPageSchema).default([]), - toolChunks: z - .record(z.string(), z.array(DocumentationChunkSchema)) - .default({}), - }) -); - -type CustomSectionsFile = z.infer; - -// ============================================================================ -// Custom Sections File Source -// ============================================================================ - -export interface CustomSectionsFileConfig { - filePath: string; -} - -const parseCustomSectionsFile = ( - content: string, - filePath: string -): CustomSectionsFile => { - let parsedJson: unknown; - try { - parsedJson = JSON.parse(content) as unknown; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw new Error( - `Custom sections file is not valid JSON (${filePath}): ${message}` - ); - } - - const parsed = CustomSectionsFileSchema.safeParse(parsedJson); - if (!parsed.success) { - throw new Error( - `Custom sections file has invalid schema (${filePath}): ${parsed.error.message}` - ); - } - - return parsed.data; -}; - -/** - * Source that loads custom documentation sections from a JSON file - */ -export class CustomSectionsFileSource implements ICustomSectionsSource { - private readonly filePath: string; - private cachedData: CustomSectionsFile | null = null; - - constructor(config: CustomSectionsFileConfig) { - this.filePath = config.filePath; - } - - private async loadFile(): Promise { - if (this.cachedData !== null) { - return this.cachedData; - } - - try { - await access(this.filePath); - const content = await readFile(this.filePath, "utf-8"); - this.cachedData = parseCustomSectionsFile(content, this.filePath); - return this.cachedData; - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - // File doesn't exist - return empty data - this.cachedData = {}; - return this.cachedData; - } - throw error; - } - } - - async getCustomSections(toolkitId: string): Promise { - const data = await this.loadFile(); - - // Try exact match - if (data[toolkitId]) { - return data[toolkitId]; - } - - // Try normalized match - const normalizedId = normalizeId(toolkitId); - const entry = Object.entries(data).find( - ([key]) => normalizeId(key) === normalizedId - ); - - return entry ? entry[1] : null; - } - - async getAllCustomSections(): Promise< - Readonly> - > { - const data = await this.loadFile(); - return data; - } -} - -// ============================================================================ -// Factory -// ============================================================================ - -export const createCustomSectionsFileSource = ( - filePath: string -): ICustomSectionsSource => new CustomSectionsFileSource({ filePath }); diff --git a/toolkit-docs-generator/src/sources/index.ts b/toolkit-docs-generator/src/sources/index.ts index 9354aa112..7b9c7046d 100644 --- a/toolkit-docs-generator/src/sources/index.ts +++ b/toolkit-docs-generator/src/sources/index.ts @@ -4,11 +4,11 @@ export * from "./arcade-api"; export * from "./arcade-api-types"; -export * from "./custom-sections-file"; export * from "./design-system-metadata"; export * from "./engine-api"; export * from "./in-memory"; export * from "./interfaces"; +export * from "./markdown-curation"; export * from "./mock-engine-api"; export * from "./mock-metadata"; export * from "./oauth-provider-resolver"; diff --git a/toolkit-docs-generator/src/sources/interfaces.ts b/toolkit-docs-generator/src/sources/interfaces.ts index 82662645d..49991117e 100644 --- a/toolkit-docs-generator/src/sources/interfaces.ts +++ b/toolkit-docs-generator/src/sources/interfaces.ts @@ -13,7 +13,7 @@ import type { CustomSections } from "../types/index"; * Interface for fetching custom documentation sections * * Implementations: - * - CustomSectionsFileSource: Loads from extracted JSON file + * - MarkdownCurationSource: Compiles hand-authored Markdown and MDX * - EmptyCustomSectionsSource: Returns empty sections (for new toolkits) */ export interface ICustomSectionsSource { diff --git a/toolkit-docs-generator/src/sources/markdown-curation.ts b/toolkit-docs-generator/src/sources/markdown-curation.ts new file mode 100644 index 000000000..ae6a2d5d3 --- /dev/null +++ b/toolkit-docs-generator/src/sources/markdown-curation.ts @@ -0,0 +1,384 @@ +/** + * Compile hand-authored Markdown and MDX into the generator's structured + * custom-sections contract. + * + * A configured curation root is authoritative for every toolkit. Missing + * toolkit directories therefore resolve to empty custom sections instead of + * falling back to prose embedded in a previous generated artifact. + */ +import { compile } from "@mdx-js/mdx"; +import { readdir, readFile, stat } from "fs/promises"; +import { join, relative, sep } from "path"; +import { parse as parseYaml } from "yaml"; +import { z } from "zod"; +import { + type CustomSections, + CustomSectionsSchema, + type DocumentationChunk, + DocumentationChunkSchema, + type ToolkitSubPage, +} from "../types/index"; +import { normalizeId } from "../utils/fp"; +import type { ICustomSectionsSource } from "./interfaces"; + +const FRONTMATTER_PATTERN = + /^---\r?\n(?[\s\S]*?)\r?\n---(?:\r?\n|$)(?[\s\S]*)$/; +const MARKDOWN_EXTENSIONS = new Set([".md", ".mdx"]); + +const ChunkFrontmatterSchema = DocumentationChunkSchema.omit({ + content: true, +}) + .extend({ + /** Optional fully qualified tool name for a tool-level chunk. */ + tool: z.string().min(1).optional(), + }) + .strict(); + +const PageFrontmatterSchema = z + .object({ + type: z.string().min(1), + }) + .strict(); + +const ImportFrontmatterSchema = z + .object({ + type: z.literal("import"), + }) + .strict(); + +type MarkdownDocument = { + body: string; + frontmatter: unknown; +}; + +type CompiledChunk = { + chunk: DocumentationChunk; + sourcePath: string; + toolName?: string; +}; + +const emptyCustomSections = (): CustomSections => + CustomSectionsSchema.parse({}); + +const extensionOf = (fileName: string): string => { + const dot = fileName.lastIndexOf("."); + return dot === -1 ? "" : fileName.slice(dot).toLowerCase(); +}; + +const isMarkdownFile = (fileName: string): boolean => + MARKDOWN_EXTENSIONS.has(extensionOf(fileName)); + +const parseMarkdownDocument = ( + source: string, + sourcePath: string +): MarkdownDocument => { + const match = source.match(FRONTMATTER_PATTERN); + if (!(match?.groups?.frontmatter && match.groups.body !== undefined)) { + throw new Error( + `Curation document must start with YAML frontmatter (${sourcePath})` + ); + } + + let frontmatter: unknown; + try { + frontmatter = parseYaml(match.groups.frontmatter); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `Curation frontmatter is invalid (${sourcePath}): ${message}` + ); + } + + const body = match.groups.body.replaceAll("\r\n", "\n").replace(/\n$/, ""); + if (body.trim().length === 0) { + throw new Error(`Curation document body is empty (${sourcePath})`); + } + + return { body, frontmatter }; +}; + +const validateMdx = async (body: string, sourcePath: string): Promise => { + try { + await compile(body, { development: false }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `Curation document has invalid MDX (${sourcePath}): ${message}` + ); + } +}; + +const parseChunk = async ( + sourcePath: string, + toolkitId: string +): Promise => { + const document = parseMarkdownDocument( + await readFile(sourcePath, "utf-8"), + sourcePath + ); + const parsed = ChunkFrontmatterSchema.safeParse(document.frontmatter); + if (!parsed.success) { + throw new Error( + `Curation chunk frontmatter has invalid schema (${sourcePath}): ${parsed.error.message}` + ); + } + await validateMdx(document.body, sourcePath); + + let toolName: string | undefined; + if (parsed.data.tool) { + const separator = parsed.data.tool.indexOf("."); + const toolToolkitId = parsed.data.tool.slice(0, separator); + toolName = parsed.data.tool.slice(separator + 1); + if ( + separator <= 0 || + toolName.length === 0 || + normalizeId(toolToolkitId) !== normalizeId(toolkitId) + ) { + throw new Error( + `Curation tool target must be fully qualified and match toolkit ${toolkitId} (${sourcePath})` + ); + } + } + + const { tool: _tool, ...metadata } = parsed.data; + return { + chunk: DocumentationChunkSchema.parse({ + ...metadata, + content: document.body, + }), + sourcePath, + ...(toolName ? { toolName } : {}), + }; +}; + +const assertSafeRelativePath = ( + relativePath: string, + sourcePath: string +): void => { + const parts = relativePath.split(/[\\/]/); + if ( + relativePath.length === 0 || + relativePath.startsWith(sep) || + parts.some((part) => part === "" || part === "." || part === "..") + ) { + throw new Error(`Curation page path is unsafe (${sourcePath})`); + } +}; + +const parsePage = async ( + sourcePath: string, + relativePath: string +): Promise => { + assertSafeRelativePath(relativePath, sourcePath); + const document = parseMarkdownDocument( + await readFile(sourcePath, "utf-8"), + sourcePath + ); + const parsed = PageFrontmatterSchema.safeParse(document.frontmatter); + if (!parsed.success) { + throw new Error( + `Curation page frontmatter has invalid schema (${sourcePath}): ${parsed.error.message}` + ); + } + await validateMdx(document.body, sourcePath); + return { + type: parsed.data.type, + content: document.body, + relativePath, + }; +}; + +const parseImport = async (sourcePath: string): Promise => { + const document = parseMarkdownDocument( + await readFile(sourcePath, "utf-8"), + sourcePath + ); + const parsed = ImportFrontmatterSchema.safeParse(document.frontmatter); + if (!parsed.success) { + throw new Error( + `Curation import frontmatter has invalid schema (${sourcePath}): ${parsed.error.message}` + ); + } + await validateMdx(document.body, sourcePath); + if (!/^import(?:\s|\{|\*)/.test(document.body)) { + throw new Error(`Curation import must be an ESM import (${sourcePath})`); + } + return document.body; +}; + +const listFilesRecursively = async (dirPath: string): Promise => { + const entries = (await readdir(dirPath, { withFileTypes: true })).sort( + (left, right) => left.name.localeCompare(right.name) + ); + const files: string[] = []; + for (const entry of entries) { + const entryPath = join(dirPath, entry.name); + if (entry.isSymbolicLink()) { + throw new Error( + `Curation directory may not contain symlinks (${entryPath})` + ); + } + if (entry.isDirectory()) { + files.push(...(await listFilesRecursively(entryPath))); + } else if (entry.isFile()) { + files.push(entryPath); + } + } + return files; +}; + +const rejectJsonFiles = (files: readonly string[]): void => { + const jsonFile = files.find((file) => extensionOf(file) === ".json"); + if (jsonFile) { + throw new Error( + `JSON curation is no longer supported; convert this file to Markdown (${jsonFile})` + ); + } +}; + +const compareChunks = (left: CompiledChunk, right: CompiledChunk): number => + left.sourcePath.localeCompare(right.sourcePath); + +const loadToolkitDirectory = async ( + toolkitPath: string, + toolkitId: string +): Promise => { + const chunksPath = join(toolkitPath, "chunks"); + const importsPath = join(toolkitPath, "imports"); + const pagesPath = join(toolkitPath, "pages"); + const allFiles = await listFilesRecursively(toolkitPath); + rejectJsonFiles(allFiles); + + const chunkFiles = allFiles.filter( + (file) => file.startsWith(`${chunksPath}${sep}`) && isMarkdownFile(file) + ); + const pageFiles = allFiles.filter( + (file) => file.startsWith(`${pagesPath}${sep}`) && isMarkdownFile(file) + ); + const importFiles = allFiles.filter( + (file) => file.startsWith(`${importsPath}${sep}`) && isMarkdownFile(file) + ); + + const chunks = ( + await Promise.all(chunkFiles.map((file) => parseChunk(file, toolkitId))) + ).sort(compareChunks); + const documentationChunks: DocumentationChunk[] = []; + const toolChunks: Record = {}; + for (const compiled of chunks) { + if (compiled.toolName) { + const chunksForTool = toolChunks[compiled.toolName] ?? []; + chunksForTool.push(compiled.chunk); + toolChunks[compiled.toolName] = chunksForTool; + } else { + documentationChunks.push(compiled.chunk); + } + } + + const subPages = await Promise.all( + pageFiles + .sort() + .map((file) => + parsePage(file, relative(pagesPath, file).split(sep).join("/")) + ) + ); + const pagePaths = subPages.map((page) => + typeof page === "string" ? page : page.relativePath.toLowerCase() + ); + if (new Set(pagePaths).size !== pagePaths.length) { + throw new Error(`Curation contains duplicate page paths (${toolkitPath})`); + } + + const customImports = await Promise.all( + importFiles.sort().map((file) => parseImport(file)) + ); + + return CustomSectionsSchema.parse({ + documentationChunks, + customImports, + subPages, + toolChunks, + }); +}; + +export class MarkdownCurationSource implements ICustomSectionsSource { + private readonly rootPath: string; + private cachedData: Readonly> | null = null; + + constructor(rootPath: string) { + this.rootPath = rootPath; + } + + private async loadData(): Promise>> { + if (this.cachedData) { + return this.cachedData; + } + + let rootStats: Awaited>; + try { + rootStats = await stat(this.rootPath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + throw new Error( + `Configured curation directory does not exist: ${this.rootPath}` + ); + } + throw error; + } + if (!rootStats.isDirectory()) { + throw new Error( + `Configured curation path is not a directory: ${this.rootPath}` + ); + } + + const entries = ( + await readdir(this.rootPath, { withFileTypes: true }) + ).sort((left, right) => left.name.localeCompare(right.name)); + const data: Record = {}; + const normalizedIds = new Map(); + for (const entry of entries) { + const entryPath = join(this.rootPath, entry.name); + if (entry.isSymbolicLink()) { + throw new Error( + `Curation directory may not contain symlinks (${entryPath})` + ); + } + if (entry.isFile() && extensionOf(entry.name) === ".json") { + rejectJsonFiles([entryPath]); + } + if (!entry.isDirectory()) { + continue; + } + const normalizedId = normalizeId(entry.name); + const duplicate = normalizedIds.get(normalizedId); + if (duplicate) { + throw new Error( + `Curation toolkit directories normalize to the same ID: ${duplicate}, ${entry.name}` + ); + } + normalizedIds.set(normalizedId, entry.name); + data[entry.name] = await loadToolkitDirectory(entryPath, entry.name); + } + + this.cachedData = data; + return data; + } + + async getCustomSections(toolkitId: string): Promise { + const data = await this.loadData(); + const normalizedId = normalizeId(toolkitId); + const entry = Object.entries(data).find( + ([key]) => normalizeId(key) === normalizedId + ); + return entry?.[1] ?? emptyCustomSections(); + } + + async getAllCustomSections(): Promise< + Readonly> + > { + return this.loadData(); + } +} + +export const createMarkdownCurationSource = ( + rootPath: string +): MarkdownCurationSource => new MarkdownCurationSource(rootPath); diff --git a/toolkit-docs-generator/tests/cli/generate-flow.test.ts b/toolkit-docs-generator/tests/cli/generate-flow.test.ts index 75257a978..dc9f06017 100644 --- a/toolkit-docs-generator/tests/cli/generate-flow.test.ts +++ b/toolkit-docs-generator/tests/cli/generate-flow.test.ts @@ -1,9 +1,14 @@ -import { describe, expect, it } from "vitest"; +import { mkdir, mkdtemp, rm } from "fs/promises"; +import { tmpdir } from "os"; +import { join } from "path"; +import { afterEach, describe, expect, it } from "vitest"; import { assertSafeCurrentToolkitSnapshot, collectRemovedToolkitIds, computeProcessingStats, filterProvidersBySkipIds, + getCombinedChangedToolkitIds, + resolveCustomSectionsPath, } from "../../src/cli/generate-flow"; import type { ChangeDetectionResult } from "../../src/diff/index"; import { assertRequireCompleteMetadata } from "../../src/merger/data-merger"; @@ -75,6 +80,43 @@ describe("collectRemovedToolkitIds", () => { }); }); +describe("getCombinedChangedToolkitIds", () => { + it("deduplicates API and curation changes case-insensitively", () => { + expect( + getCombinedChangedToolkitIds( + makeResult([{ toolkitId: "Github", changeType: "modified" }]), + ["github"] + ) + ).toEqual(["github"]); + }); +}); + +describe("resolveCustomSectionsPath", () => { + const tempDirs: string[] = []; + + afterEach(async () => { + await Promise.all( + tempDirs.splice(0).map((dir) => rm(dir, { recursive: true })) + ); + }); + + it("uses curation/ by default when it exists", async () => { + const workingDir = await mkdtemp(join(tmpdir(), "toolkit-curation-")); + tempDirs.push(workingDir); + await mkdir(join(workingDir, "curation")); + + await expect( + resolveCustomSectionsPath(undefined, workingDir) + ).resolves.toBe(join(workingDir, "curation")); + }); + + it("preserves an explicit custom-sections path", async () => { + await expect( + resolveCustomSectionsPath("./custom-prose", "/unused") + ).resolves.toBe("./custom-prose"); + }); +}); + describe("assertSafeCurrentToolkitSnapshot", () => { it("rejects an empty current snapshot when previous output exists", () => { expect(() => assertSafeCurrentToolkitSnapshot(0, 115)).toThrow( diff --git a/toolkit-docs-generator/tests/merger/data-merger.test.ts b/toolkit-docs-generator/tests/merger/data-merger.test.ts index 1dc26bd2d..d8e0a2940 100644 --- a/toolkit-docs-generator/tests/merger/data-merger.test.ts +++ b/toolkit-docs-generator/tests/merger/data-merger.test.ts @@ -10,6 +10,7 @@ import { computeAllScopes, DataMerger, determineAuthType, + getCustomSectionsSourceHash, getProviderId, groupToolsByToolkit, mergeToolkit, @@ -425,6 +426,9 @@ describe("mergeToolkit", () => { expect(result.toolkit.auth?.allScopes).toContain("scope1"); expect(result.toolkit.auth?.allScopes).toContain("scope2"); expect(result.toolkit.documentationChunks).toHaveLength(1); + expect(result.toolkit.curationSourceHash).toBe( + getCustomSectionsSourceHash(customSections) + ); expect(result.warnings).toHaveLength(0); }); @@ -693,7 +697,7 @@ describe("mergeToolkit", () => { createStubGenerator() ); - // Run again with empty custom sections - should preserve previous + // No curation file for this toolkit (null source) — carry forward. const result = await mergeToolkit("TestKit", tools, null, null, undefined, { previousToolkit: previousResult.toolkit, }); @@ -706,6 +710,42 @@ describe("mergeToolkit", () => { expect(result.toolkit.subPages).toHaveLength(1); }); + it("should clear toolkit-level custom sections when curation is authoritative but empty", async () => { + const tools = [createTool({ qualifiedName: "TestKit.Tool1" })]; + + const previousResult = await mergeToolkit( + "TestKit", + tools, + null, + createCustomSections({ + documentationChunks: [ + { + type: "warning", + location: "header", + position: "after", + content: "Important warning!", + }, + ], + customImports: ['import CustomComponent from "@/components/custom";'], + subPages: ["environment-variables"], + }), + createStubGenerator() + ); + + const result = await mergeToolkit( + "TestKit", + tools, + null, + createCustomSections(), + undefined, + { previousToolkit: previousResult.toolkit } + ); + + expect(result.toolkit.documentationChunks).toHaveLength(0); + expect(result.toolkit.customImports).toHaveLength(0); + expect(result.toolkit.subPages).toHaveLength(0); + }); + it("should use source custom sections over previous when source has content", async () => { const tools = [createTool({ qualifiedName: "TestKit.Tool1" })]; @@ -927,6 +967,31 @@ describe("mergeToolkit resolveProviderId fallback", () => { }); describe("mergeToolkit overview chunk handling", () => { + it("rejects curation that targets a tool outside the toolkit", async () => { + await expect( + mergeToolkit( + "TestKit", + [createTool()], + createMetadata(), + createCustomSections({ + toolChunks: { + MissingTool: [ + { + type: "markdown", + location: "description", + position: "after", + content: "This target is stale.", + }, + ], + }, + }), + undefined + ) + ).rejects.toThrow( + "Curation for TestKit targets unknown tool(s): MissingTool" + ); + }); + it("keeps toolkit-level overview chunks from source custom sections", async () => { const result = await mergeToolkit( "TestKit", @@ -1513,6 +1578,59 @@ describe("DataMerger", () => { ).toBe(true); }); + it("warns without rewriting an authoritative curation chunk", async () => { + const currentTool = createTool({ + name: "CreateIssue", + qualifiedName: "Github.CreateIssue", + fullyQualifiedName: "Github.CreateIssue@1.0.0", + secrets: [], + }); + const previous = await mergeToolkit( + "Github", + [createTool({ ...currentTool, secrets: ["OLD_SECRET"] })], + githubMetadata, + null, + createStubGenerator() + ); + const cleanupSpy = vi.fn(async () => "Rewritten content"); + const merger = new DataMerger({ + toolkitDataSource: createCombinedToolkitDataSource({ + toolSource: new InMemoryToolDataSource([currentTool]), + metadataSource: new InMemoryMetadataSource([githubMetadata]), + }), + customSectionsSource: new InMemoryCustomSectionsSource({ + Github: createCustomSections({ + documentationChunks: [ + { + type: "warning", + location: "description", + position: "after", + content: "Source still mentions OLD_SECRET.", + }, + ], + }), + }), + toolExampleGenerator: createStubGenerator(), + secretEditGenerator: { + cleanupStaleReferences: cleanupSpy, + fillCoverageGaps: vi.fn(async ({ content }) => content), + }, + previousToolkits: new Map([["github", previous.toolkit]]), + }); + + const result = await merger.mergeToolkit("Github"); + + expect(cleanupSpy).not.toHaveBeenCalled(); + expect(result.toolkit.documentationChunks[0]?.content).toBe( + "Source still mentions OLD_SECRET." + ); + expect( + result.warnings.some((warning) => + warning.includes("Stale secret reference") + ) + ).toBe(true); + }); + it("passes the post-cleanup summary to the coverage editor, not the original", async () => { // Ordering guarantee: applyStaleRefCleanup runs before the coverage // scan is re-computed. We prove this by making cleanup mutate a @@ -1905,10 +2023,7 @@ describe("DataMerger", () => { }); }); - describe("error handling preserves previous custom sections", () => { - // buildMergeErrorResult is invoked by mergeToolkitEntry (called from - // mergeAllToolkits). We trigger it by making the customSectionsSource throw, - // which is caught by mergeToolkitEntry's try/catch. + describe("curation configuration errors", () => { const makeFailingCustomSectionsSource = (): ICustomSectionsSource => ({ getCustomSections: async () => { throw new Error("Custom sections source unavailable"); @@ -1918,7 +2033,7 @@ describe("DataMerger", () => { }, }); - it("preserves documentationChunks and customImports from previous toolkit when merge throws", async () => { + it("fails before recovery instead of preserving stale prose", async () => { const toolkitDataSource = createCombinedToolkitDataSource({ toolSource: new InMemoryToolDataSource([githubTool1]), metadataSource: new InMemoryMetadataSource([githubMetadata]), @@ -1942,62 +2057,16 @@ describe("DataMerger", () => { }), createStubGenerator() ); - const completedToolkitIds: string[] = []; - const merger = new DataMerger({ toolkitDataSource, customSectionsSource: makeFailingCustomSectionsSource(), toolExampleGenerator: createStubGenerator(), previousToolkits: new Map([["github", previousResult.toolkit]]), - onToolkitComplete: async (result) => { - completedToolkitIds.push(result.toolkit.id); - }, - }); - - const results = await merger.mergeAllToolkits(); - const result = results[0]; - - expect(result?.error).toBe("Custom sections source unavailable"); - expect(result?.toolkit).toEqual(previousResult.toolkit); - expect(result?.toolkit.documentationChunks).toHaveLength(1); - expect(result?.toolkit.documentationChunks[0]?.content).toBe( - "Critical: GitHub Apps only." - ); - expect(result?.toolkit.customImports).toHaveLength(1); - expect(result?.toolkit.subPages).toEqual(["setup-guide"]); - expect(result?.warnings[0]).toContain( - "Custom sections source unavailable" - ); - expect(completedToolkitIds).toEqual(["Github"]); - }); - - it("returns empty custom sections in error result when no previous toolkit exists", async () => { - const toolkitDataSource = createCombinedToolkitDataSource({ - toolSource: new InMemoryToolDataSource([githubTool1]), - metadataSource: new InMemoryMetadataSource([githubMetadata]), - }); - const completedToolkitIds: string[] = []; - - const merger = new DataMerger({ - toolkitDataSource, - customSectionsSource: makeFailingCustomSectionsSource(), - toolExampleGenerator: createStubGenerator(), - onToolkitComplete: async (result) => { - completedToolkitIds.push(result.toolkit.id); - }, }); - const results = await merger.mergeAllToolkits(); - const result = results[0]; - - expect(result?.error).toBe("Custom sections source unavailable"); - expect(result?.toolkit.documentationChunks).toHaveLength(0); - expect(result?.toolkit.customImports).toHaveLength(0); - expect(result?.toolkit.subPages).toHaveLength(0); - expect(result?.warnings[0]).toContain( + await expect(merger.mergeAllToolkits()).rejects.toThrow( "Custom sections source unavailable" ); - expect(completedToolkitIds).toEqual([]); }); }); @@ -2103,6 +2172,147 @@ describe("DataMerger", () => { expect(result?.error).toContain("missing design-system metadata"); }); + it("overlays authoritative empty curation on preserved prior output", async () => { + const previous = await mergeToolkit( + "Github", + [githubTool1], + githubMetadata, + createCustomSections({ + documentationChunks: [ + { + type: "warning", + location: "description", + position: "after", + content: "Delete me", + }, + ], + customImports: ["import Old from 'old';"], + subPages: ["old-page"], + toolChunks: { + CreateIssue: [ + { + type: "info", + location: "parameters", + position: "after", + content: "Delete this too", + }, + ], + }, + }), + createStubGenerator() + ); + const toolkitDataSource = createCombinedToolkitDataSource({ + toolSource: new InMemoryToolDataSource([githubTool1]), + metadataSource: new InMemoryMetadataSource([]), + }); + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: new InMemoryCustomSectionsSource({ + Github: createCustomSections(), + }), + toolExampleGenerator: createStubGenerator(), + previousToolkits: new Map([["github", previous.toolkit]]), + preserveLastKnownGood: true, + }); + + const [result] = await merger.mergeAllToolkits(); + + expect(result?.recovery).toBe("preserved"); + expect(result?.toolkit.documentationChunks).toEqual([]); + expect(result?.toolkit.customImports).toEqual([]); + expect(result?.toolkit.subPages).toEqual([]); + expect(result?.toolkit.tools[0]?.documentationChunks).toEqual([]); + expect(result?.toolkit.tools[0]?.codeExample).toEqual( + previous.toolkit.tools[0]?.codeExample + ); + }); + + it("preserves prior output when curation targets a tool absent from it", async () => { + const previous = await mergeToolkit( + "Github", + [githubTool1], + githubMetadata, + createCustomSections(), + createStubGenerator() + ); + const curation = createCustomSections({ + toolChunks: { + SetStarred: [ + { + type: "info", + location: "parameters", + position: "after", + content: "Applies once the prior artifact catches up.", + }, + ], + }, + }); + // The API exposes SetStarred, so the curation is valid; only the + // preserved artifact predates it. + const toolkitDataSource = createCombinedToolkitDataSource({ + toolSource: new InMemoryToolDataSource([githubTool1, githubTool2]), + metadataSource: new InMemoryMetadataSource([]), + }); + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: new InMemoryCustomSectionsSource({ + Github: curation, + }), + toolExampleGenerator: createStubGenerator(), + previousToolkits: new Map([["github", previous.toolkit]]), + preserveLastKnownGood: true, + }); + + const [result] = await merger.mergeAllToolkits(); + + expect(result?.recovery).toBe("preserved"); + expect(result?.toolkit.tools).toHaveLength(1); + expect(result?.toolkit.curationSourceHash).toBe( + getCustomSectionsSourceHash(curation) + ); + }); + + it("fails the run when curation targets a tool the API does not expose, even with preserveLastKnownGood", async () => { + // A mistyped `tool:` target is an authoring mistake, not an upstream + // outage. Recovering from it would leave the nightly job green while + // the toolkit kept stale data and lost the chunk. + const previous = await mergeToolkit( + "Github", + [githubTool1], + githubMetadata, + createCustomSections(), + createStubGenerator() + ); + const toolkitDataSource = createCombinedToolkitDataSource({ + toolSource: new InMemoryToolDataSource([githubTool1]), + metadataSource: new InMemoryMetadataSource([githubMetadata]), + }); + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: new InMemoryCustomSectionsSource({ + Github: createCustomSections({ + toolChunks: { + CreateIsue: [ + { + type: "info", + location: "parameters", + position: "after", + content: "Typo in the target tool name.", + }, + ], + }, + }), + }), + toolExampleGenerator: createStubGenerator(), + previousToolkits: new Map([["github", previous.toolkit]]), + preserveLastKnownGood: true, + }); + + await expect(merger.mergeAllToolkits()).rejects.toThrow( + "Curation for Github targets unknown tool(s): CreateIsue" + ); + }); + it("preserves prior output for provider-mode generation when metadata is missing", async () => { const previous = await mergeToolkit( "Github", @@ -2300,7 +2510,7 @@ describe("DataMerger", () => { }); await expect(merger.mergeAllToolkits()).rejects.toThrow( - "Failed to process Github: Custom sections source unavailable" + "Custom sections source unavailable" ); }); diff --git a/toolkit-docs-generator/tests/scenarios/curation-corpus.test.ts b/toolkit-docs-generator/tests/scenarios/curation-corpus.test.ts new file mode 100644 index 000000000..2519ad8e7 --- /dev/null +++ b/toolkit-docs-generator/tests/scenarios/curation-corpus.test.ts @@ -0,0 +1,49 @@ +import { readdir, readFile } from "fs/promises"; +import { join } from "path"; +import { describe, expect, it } from "vitest"; +import { createMarkdownCurationSource } from "../../src/sources/markdown-curation"; +import type { MergedToolkit } from "../../src/types/index"; + +const GENERATOR_ROOT = join(__dirname, "../.."); +const CURATION_DIR = join(GENERATOR_ROOT, "curation"); +const TOOLKITS_DIR = join(GENERATOR_ROOT, "data", "toolkits"); + +describe("checked-in Markdown curation", () => { + it("reproduces every authored field in committed toolkit data", async () => { + const source = createMarkdownCurationSource(CURATION_DIR); + const files = (await readdir(TOOLKITS_DIR)) + .filter((file) => file.endsWith(".json") && file !== "index.json") + .sort(); + + let chunkCount = 0; + let subPageCount = 0; + for (const file of files) { + const toolkit = JSON.parse( + await readFile(join(TOOLKITS_DIR, file), "utf-8") + ) as MergedToolkit; + const current = await source.getCustomSections(toolkit.id); + const expectedToolChunks = Object.fromEntries( + toolkit.tools + .filter((tool) => tool.documentationChunks.length > 0) + .map((tool) => [tool.name, tool.documentationChunks]) + ); + + expect(current.documentationChunks, file).toEqual( + toolkit.documentationChunks + ); + expect(current.toolChunks, file).toEqual(expectedToolChunks); + expect(current.subPages, file).toEqual(toolkit.subPages); + expect(current.customImports, file).toEqual(toolkit.customImports); + + chunkCount += current.documentationChunks.length; + chunkCount += Object.values(current.toolChunks).reduce( + (total, chunks) => total + chunks.length, + 0 + ); + subPageCount += current.subPages.length; + } + + expect(chunkCount).toBe(82); + expect(subPageCount).toBe(2); + }); +}); diff --git a/toolkit-docs-generator/tests/scenarios/custom-sections-diff.test.ts b/toolkit-docs-generator/tests/scenarios/custom-sections-diff.test.ts new file mode 100644 index 000000000..e8f05de31 --- /dev/null +++ b/toolkit-docs-generator/tests/scenarios/custom-sections-diff.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, it } from "vitest"; +import { getChangedToolkitIdsFromCustomSections } from "../../src/diff/index"; +import { getCustomSectionsSourceHash } from "../../src/merger/data-merger"; +import type { MergedToolkit } from "../../src/types/index"; + +const previousToolkit = (documentation = "old"): MergedToolkit => ({ + id: "Github", + label: "Github", + version: "1.0.0", + description: "GitHub", + metadata: { + category: "development", + iconUrl: "https://example.com/icon.svg", + isBYOC: false, + isPro: false, + type: "arcade", + docsLink: "https://docs.example.com", + isComingSoon: false, + isHidden: false, + }, + auth: null, + tools: [], + documentationChunks: [ + { + type: "warning", + location: "description", + position: "after", + content: documentation, + }, + ], + customImports: [], + subPages: [], + generatedAt: "2026-01-01T00:00:00.000Z", +}); + +describe("getChangedToolkitIdsFromCustomSections", () => { + it("treats curation-only edits as toolkit changes", () => { + expect( + getChangedToolkitIdsFromCustomSections( + { + github: { + documentationChunks: [ + { + type: "warning", + location: "description", + position: "after", + content: "new", + }, + ], + customImports: [], + subPages: [], + toolChunks: {}, + }, + }, + new Map([["Github", previousToolkit()]]) + ) + ).toEqual(["github"]); + }); + + it("does not report identical curation", () => { + const toolkit = previousToolkit(); + expect( + getChangedToolkitIdsFromCustomSections( + { + Github: { + documentationChunks: toolkit.documentationChunks, + customImports: [], + subPages: [], + toolChunks: {}, + }, + }, + new Map([["Github", toolkit]]) + ) + ).toEqual([]); + }); + + it("uses the curation fingerprint after generated prose is edited", () => { + const curation = { + documentationChunks: [ + { + type: "warning" as const, + location: "description" as const, + position: "after" as const, + content: "hand-authored source", + }, + ], + customImports: [], + subPages: [], + toolChunks: {}, + }; + const toolkit = previousToolkit("secret-coherence edited output"); + toolkit.curationSourceHash = getCustomSectionsSourceHash(curation); + + expect( + getChangedToolkitIdsFromCustomSections( + { github: curation }, + new Map([["Github", toolkit]]) + ) + ).toEqual([]); + }); + + it("treats cleared curation as a prose change", () => { + expect( + getChangedToolkitIdsFromCustomSections( + { + github: { + documentationChunks: [], + customImports: [], + subPages: [], + toolChunks: {}, + }, + }, + new Map([["Github", previousToolkit()]]) + ) + ).toEqual(["github"]); + }); + + it("treats a missing toolkit directory as cleared curation", () => { + expect( + getChangedToolkitIdsFromCustomSections( + {}, + new Map([["Github", previousToolkit()]]) + ) + ).toEqual(["github"]); + }); + + it("ignores absent curation when the previous artifact is also empty", () => { + const toolkit = previousToolkit(); + toolkit.documentationChunks = []; + + expect( + getChangedToolkitIdsFromCustomSections({}, new Map([["Github", toolkit]])) + ).toEqual([]); + }); +}); diff --git a/toolkit-docs-generator/tests/scenarios/prose-survives-force-regenerate.test.ts b/toolkit-docs-generator/tests/scenarios/prose-survives-force-regenerate.test.ts new file mode 100644 index 000000000..b7ee5c813 --- /dev/null +++ b/toolkit-docs-generator/tests/scenarios/prose-survives-force-regenerate.test.ts @@ -0,0 +1,182 @@ +/** + * Scenario Test: Hand-authored prose survives --force-regenerate + * + * `documentationChunks`, `customImports`, and `subPages` have no upstream + * source. Before curation files, they survived only by carry-forward from the + * previous artifact — and `--force-regenerate` / `--overwrite-output` set the + * previous-output directory to undefined, discarding all of it. + * + * These tests reproduce the force-regenerate condition (no previous toolkit) + * and assert that prose loaded from a `curation/` directory still lands in the + * merged output. The final test pins the old bug: with no previous toolkit and + * no curation, the prose is gone. + */ +import { mkdir, mkdtemp, rm, writeFile } from "fs/promises"; +import { tmpdir } from "os"; +import { join } from "path"; +import { afterEach, describe, expect, it } from "vitest"; +import { mergeToolkit } from "../../src/merger/data-merger"; +import { createMarkdownCurationSource } from "../../src/sources/markdown-curation"; +import type { ToolDefinition } from "../../src/types/index"; + +const createTool = (): ToolDefinition => ({ + name: "TestTool", + qualifiedName: "TestKit.TestTool", + fullyQualifiedName: "TestKit.TestTool@1.0.0", + description: "A test tool", + toolkitDescription: "Toolkit description", + parameters: [], + auth: null, + secrets: [], + output: { type: "object", description: "Result" }, +}); + +const writeCuration = async (root: string): Promise => { + await mkdir(join(root, "testkit/chunks"), { recursive: true }); + await mkdir(join(root, "testkit/imports"), { recursive: true }); + await mkdir(join(root, "testkit/pages/environment-variables"), { + recursive: true, + }); + await writeFile( + join(root, "testkit/chunks/guidance.mdx"), + `--- +type: warning +location: description +position: after +--- +Hand-authored guidance that has no upstream source. +` + ); + await writeFile( + join(root, "testkit/pages/environment-variables/page.mdx"), + `--- +type: environment-variables +--- +# Environment Variables +` + ); + await writeFile( + join(root, "testkit/imports/starter-tool-info.mdx"), + `--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; +` + ); +}; + +describe("prose survives --force-regenerate", () => { + let tempDir: string | null = null; + + afterEach(async () => { + if (tempDir) { + await rm(tempDir, { recursive: true, force: true }); + tempDir = null; + } + }); + + it("keeps curation prose when there is no previous toolkit to carry forward", async () => { + tempDir = await mkdtemp(join(tmpdir(), "curation-")); + await writeCuration(tempDir); + + const source = createMarkdownCurationSource(tempDir); + const customSections = await source.getCustomSections("TestKit"); + expect(customSections).not.toBeNull(); + + // previousToolkit undefined == what --force-regenerate produces. + const result = await mergeToolkit( + "TestKit", + [createTool()], + null, + customSections, + undefined, + { previousToolkit: undefined } + ); + + expect(result.toolkit.documentationChunks).toHaveLength(1); + expect(result.toolkit.documentationChunks[0]?.content).toBe( + "Hand-authored guidance that has no upstream source." + ); + expect(result.toolkit.customImports).toEqual([ + 'import StarterToolInfo from "@/app/_components/starter-tool-info";', + ]); + expect(result.toolkit.subPages).toEqual([ + { + type: "environment-variables", + content: "# Environment Variables", + relativePath: "environment-variables/page.mdx", + }, + ]); + }); + + it("normalizes the toolkit id when matching curation files", async () => { + tempDir = await mkdtemp(join(tmpdir(), "curation-")); + await mkdir(join(tempDir, "notiontoolkit/chunks"), { recursive: true }); + await writeFile( + join(tempDir, "notiontoolkit/chunks/guidance.mdx"), + `--- +type: warning +location: description +position: after +--- +Prose +` + ); + + const source = createMarkdownCurationSource(tempDir); + // File stem "notiontoolkit" must match toolkit id "NotionToolkit". + const customSections = await source.getCustomSections("NotionToolkit"); + + expect(customSections?.documentationChunks).toHaveLength(1); + }); + + it("loses prose without curation and without a previous toolkit (the bug)", async () => { + const result = await mergeToolkit( + "TestKit", + [createTool()], + null, + null, + undefined, + { previousToolkit: undefined } + ); + + expect(result.toolkit.documentationChunks).toHaveLength(0); + expect(result.toolkit.customImports).toHaveLength(0); + expect(result.toolkit.subPages).toHaveLength(0); + }); + + it("clears prose when the toolkit directory is deleted", async () => { + tempDir = await mkdtemp(join(tmpdir(), "curation-")); + await writeCuration(tempDir); + + const withProse = + await createMarkdownCurationSource(tempDir).getCustomSections("TestKit"); + + const previousResult = await mergeToolkit( + "TestKit", + [createTool()], + null, + withProse, + undefined, + { previousToolkit: undefined } + ); + expect(previousResult.toolkit.documentationChunks).toHaveLength(1); + + await rm(join(tempDir, "testkit"), { recursive: true, force: true }); + const clearedCuration = + await createMarkdownCurationSource(tempDir).getCustomSections("TestKit"); + + const result = await mergeToolkit( + "TestKit", + [createTool()], + null, + clearedCuration, + undefined, + { previousToolkit: previousResult.toolkit } + ); + + expect(result.toolkit.documentationChunks).toHaveLength(0); + expect(result.toolkit.customImports).toHaveLength(0); + expect(result.toolkit.subPages).toHaveLength(0); + }); +}); diff --git a/toolkit-docs-generator/tests/sources/custom-sections-file.test.ts b/toolkit-docs-generator/tests/sources/custom-sections-file.test.ts deleted file mode 100644 index 8923abf80..000000000 --- a/toolkit-docs-generator/tests/sources/custom-sections-file.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { mkdtemp, rm, writeFile } from "fs/promises"; -import { tmpdir } from "os"; -import { join } from "path"; -import { afterEach, describe, expect, it } from "vitest"; -import { createCustomSectionsFileSource } from "../../src/sources/custom-sections-file"; - -const createTempDir = async (): Promise => - mkdtemp(join(tmpdir(), "custom-sections-")); - -describe("CustomSectionsFileSource", () => { - let tempDir: string | null = null; - - afterEach(async () => { - if (tempDir) { - await rm(tempDir, { recursive: true, force: true }); - tempDir = null; - } - }); - - it("returns empty data when file is missing", async () => { - tempDir = await createTempDir(); - const filePath = join(tempDir, "missing.json"); - const source = createCustomSectionsFileSource(filePath); - - const result = await source.getCustomSections("Github"); - expect(result).toBeNull(); - - const all = await source.getAllCustomSections(); - expect(all).toEqual({}); - }); - - it("loads custom sections with defaults applied", async () => { - tempDir = await createTempDir(); - const filePath = join(tempDir, "custom-sections.json"); - await writeFile( - filePath, - JSON.stringify( - { - Github: {}, - }, - null, - 2 - ) - ); - - const source = createCustomSectionsFileSource(filePath); - const result = await source.getCustomSections("Github"); - - expect(result).not.toBeNull(); - expect(result?.documentationChunks).toEqual([]); - expect(result?.customImports).toEqual([]); - expect(result?.subPages).toEqual([]); - expect(result?.toolChunks).toEqual({}); - }); - - it("loads rich subpage entries supported by generated toolkit output", async () => { - tempDir = await createTempDir(); - const filePath = join(tempDir, "custom-sections.json"); - const subPage = { - type: "mdx", - content: "# Setup", - relativePath: "setup/page.mdx", - }; - await writeFile( - filePath, - JSON.stringify({ Github: { subPages: [subPage] } }, null, 2) - ); - - const source = createCustomSectionsFileSource(filePath); - const result = await source.getCustomSections("Github"); - - expect(result?.subPages).toEqual([subPage]); - }); - - it("throws a helpful error when JSON is invalid", async () => { - tempDir = await createTempDir(); - const filePath = join(tempDir, "invalid.json"); - await writeFile(filePath, "{ invalid-json"); - - const source = createCustomSectionsFileSource(filePath); - - await expect(source.getAllCustomSections()).rejects.toThrow( - `Custom sections file is not valid JSON (${filePath})` - ); - }); - - it("throws a helpful error when schema is invalid", async () => { - tempDir = await createTempDir(); - const filePath = join(tempDir, "invalid-schema.json"); - await writeFile( - filePath, - JSON.stringify( - { - Github: { - documentationChunks: "not-an-array", - }, - }, - null, - 2 - ) - ); - - const source = createCustomSectionsFileSource(filePath); - - await expect(source.getAllCustomSections()).rejects.toThrow( - `Custom sections file has invalid schema (${filePath})` - ); - }); - - it("rejects malformed rich subpage entries", async () => { - tempDir = await createTempDir(); - const filePath = join(tempDir, "invalid-subpage.json"); - await writeFile( - filePath, - JSON.stringify({ Github: { subPages: [{ type: "mdx" }] } }, null, 2) - ); - - const source = createCustomSectionsFileSource(filePath); - - await expect(source.getAllCustomSections()).rejects.toThrow( - `Custom sections file has invalid schema (${filePath})` - ); - }); -}); diff --git a/toolkit-docs-generator/tests/sources/markdown-curation.test.ts b/toolkit-docs-generator/tests/sources/markdown-curation.test.ts new file mode 100644 index 000000000..2a0e67b04 --- /dev/null +++ b/toolkit-docs-generator/tests/sources/markdown-curation.test.ts @@ -0,0 +1,234 @@ +import { mkdir, mkdtemp, rm, symlink, writeFile } from "fs/promises"; +import { tmpdir } from "os"; +import { join } from "path"; +import { afterEach, describe, expect, it } from "vitest"; +import { createMarkdownCurationSource } from "../../src/sources/markdown-curation"; + +const createTempDir = async (): Promise => + mkdtemp(join(tmpdir(), "markdown-curation-")); + +const writeDocument = async ( + root: string, + relativePath: string, + source: string +): Promise => { + const filePath = join(root, relativePath); + await mkdir(join(filePath, ".."), { recursive: true }); + await writeFile(filePath, source); +}; + +const chunk = (overrides = "", body = "Authored prose"): string => `--- +type: warning +location: description +position: after +${overrides}--- +${body} +`; + +describe("MarkdownCurationSource", () => { + let tempDir: string | null = null; + + afterEach(async () => { + if (tempDir) { + await rm(tempDir, { recursive: true, force: true }); + tempDir = null; + } + }); + + it("fails when the configured root is missing", async () => { + tempDir = await createTempDir(); + const source = createMarkdownCurationSource(join(tempDir, "missing")); + + await expect(source.getAllCustomSections()).rejects.toThrow( + "Configured curation directory does not exist" + ); + }); + + it("returns authoritative empty sections for a missing toolkit", async () => { + tempDir = await createTempDir(); + const source = createMarkdownCurationSource(tempDir); + + await expect(source.getCustomSections("Github")).resolves.toEqual({ + documentationChunks: [], + customImports: [], + subPages: [], + toolChunks: {}, + }); + }); + + it("compiles toolkit and tool chunks from Markdown", async () => { + tempDir = await createTempDir(); + await writeDocument( + tempDir, + "github/chunks/01-toolkit.mdx", + chunk("header: '## Setup'\n", "## Setup\n\nRead this.") + ); + await writeDocument( + tempDir, + "github/chunks/02-tool.mdx", + chunk("tool: Github.CreateIssue\n", "Tool guidance") + ); + + const sections = + await createMarkdownCurationSource(tempDir).getCustomSections("GitHub"); + + expect(sections.documentationChunks).toEqual([ + expect.objectContaining({ + header: "## Setup", + content: "## Setup\n\nRead this.", + }), + ]); + expect(sections.toolChunks.CreateIssue).toEqual([ + expect.objectContaining({ content: "Tool guidance" }), + ]); + expect(sections.customImports).toEqual([]); + }); + + it("compiles nested subpages and derives their relative paths", async () => { + tempDir = await createTempDir(); + await writeDocument( + tempDir, + "jira/pages/environment-variables/page.mdx", + `--- +type: environment-variables +--- +# Environment variables +` + ); + + const sections = + await createMarkdownCurationSource(tempDir).getCustomSections("Jira"); + + expect(sections.subPages).toEqual([ + { + type: "environment-variables", + content: "# Environment variables", + relativePath: "environment-variables/page.mdx", + }, + ]); + }); + + it("loads custom imports from Markdown", async () => { + tempDir = await createTempDir(); + await writeDocument( + tempDir, + "github/imports/01-starter-tool-info.mdx", + `--- +type: import +--- +import StarterToolInfo from "@/app/_components/starter-tool-info"; +` + ); + + const sections = + await createMarkdownCurationSource(tempDir).getCustomSections("Github"); + + expect(sections.customImports).toEqual([ + 'import StarterToolInfo from "@/app/_components/starter-tool-info";', + ]); + }); + + it("orders chunks by source path while preserving priority metadata", async () => { + tempDir = await createTempDir(); + await writeDocument( + tempDir, + "github/chunks/b.mdx", + chunk("priority: 20\n", "Second") + ); + await writeDocument( + tempDir, + "github/chunks/c.mdx", + chunk("priority: 10\n", "First") + ); + await writeDocument( + tempDir, + "github/chunks/a.mdx", + chunk("priority: 20\n", "Middle") + ); + + const sections = + await createMarkdownCurationSource(tempDir).getCustomSections("Github"); + + expect( + sections.documentationChunks.map(({ content, priority }) => ({ + content, + priority, + })) + ).toEqual([ + { content: "Middle", priority: 20 }, + { content: "Second", priority: 20 }, + { content: "First", priority: 10 }, + ]); + }); + + it("rejects leftover JSON curation", async () => { + tempDir = await createTempDir(); + await writeFile(join(tempDir, "github.json"), "{}"); + + await expect( + createMarkdownCurationSource(tempDir).getAllCustomSections() + ).rejects.toThrow("JSON curation is no longer supported"); + }); + + it("rejects invalid frontmatter with the source path", async () => { + tempDir = await createTempDir(); + const filePath = join(tempDir, "github/chunks/bad.mdx"); + await writeDocument( + tempDir, + "github/chunks/bad.mdx", + chunk("unknown: true\n") + ); + + await expect( + createMarkdownCurationSource(tempDir).getAllCustomSections() + ).rejects.toThrow(`invalid schema (${filePath})`); + }); + + it("rejects malformed MDX with the source path", async () => { + tempDir = await createTempDir(); + const filePath = join(tempDir, "github/chunks/bad.mdx"); + await writeDocument( + tempDir, + "github/chunks/bad.mdx", + chunk("", "Unclosed") + ); + + await expect( + createMarkdownCurationSource(tempDir).getAllCustomSections() + ).rejects.toThrow(`invalid MDX (${filePath})`); + }); + + it("rejects a tool target from another toolkit", async () => { + tempDir = await createTempDir(); + await writeDocument( + tempDir, + "github/chunks/bad-tool.mdx", + chunk("tool: Slack.SendMessage\n") + ); + + await expect( + createMarkdownCurationSource(tempDir).getAllCustomSections() + ).rejects.toThrow("must be fully qualified and match toolkit github"); + }); + + it("rejects normalized toolkit directory collisions", async () => { + tempDir = await createTempDir(); + await mkdir(join(tempDir, "NotionToolkit")); + await mkdir(join(tempDir, "notion-toolkit")); + + await expect( + createMarkdownCurationSource(tempDir).getAllCustomSections() + ).rejects.toThrow("normalize to the same ID"); + }); + + it("rejects symlinked toolkit directories", async () => { + tempDir = await createTempDir(); + const target = join(tempDir, "target"); + await mkdir(target); + await symlink(target, join(tempDir, "github")); + + await expect( + createMarkdownCurationSource(tempDir).getAllCustomSections() + ).rejects.toThrow("may not contain symlinks"); + }); +}); diff --git a/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts b/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts index 94990321d..2e739f501 100644 --- a/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts +++ b/toolkit-docs-generator/tests/workflows/generate-toolkit-docs.test.ts @@ -38,6 +38,7 @@ test("porter workflow generates docs and opens a PR", () => { expect(workflowContents).toContain("--llm-max-tokens 8192"); expect(workflowContents).toContain("--exclude-file ./remove-toolkits.txt"); expect(workflowContents).toContain("--ignore-file ./skip-toolkits.txt"); + expect(workflowContents).toContain("--custom-sections ./curation"); expect(workflowContents).toContain("--remove-empty-sections=false"); expect(workflowContents).toContain("peter-evans/create-pull-request"); expect(workflowContents).toContain("HUSKY: 0");