diff --git a/.github/actions/app-token/action.yml b/.github/actions/app-token/action.yml deleted file mode 100644 index c811148ba..000000000 --- a/.github/actions/app-token/action.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Get app token -description: > - Mint a short-lived installation token for the docs automation GitHub App and - resolve the git identity of its bot user. - -inputs: - app-id: - description: App ID or client ID of the docs automation app. - required: true - private-key: - description: Private key of the docs automation app. - required: true - -outputs: - token: - description: Installation token, scoped to this repository and valid for one hour. - value: ${{ steps.app-token.outputs.token }} - user-name: - description: Git user name of the app's bot user. - value: ${{ steps.bot-user.outputs.user-name }} - user-email: - description: Git email of the app's bot user, which links commits to the bot account. - value: ${{ steps.bot-user.outputs.user-email }} - committer: - description: Bot identity as a single `Name ` string, the form create-pull-request expects. - value: ${{ steps.bot-user.outputs.committer }} - -runs: - using: composite - steps: - - name: Mint an installation token - id: app-token - uses: actions/create-github-app-token@v2 - with: - # `app-id` takes either the numeric app ID or the app's client ID. - app-id: ${{ inputs.app-id }} - private-key: ${{ inputs.private-key }} - - - name: Resolve the bot user identity - id: bot-user - shell: bash - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - APP_SLUG: ${{ steps.app-token.outputs.app-slug }} - run: | - # Assign before echoing. `echo "id=$(gh api ...)"` reports echo's exit - # code, so a failed lookup would pass the step with an empty id, and - # the resulting address is one git accepts but GitHub cannot link to - # the bot — misattributed commits, no error anywhere. - id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) - - name="${APP_SLUG}[bot]" - email="${id}+${APP_SLUG}[bot]@users.noreply.github.com" - - { - echo "user-name=${name}" - echo "user-email=${email}" - echo "committer=${name} <${email}>" - } >>"$GITHUB_OUTPUT" diff --git a/.github/workflows/context7.yml b/.github/workflows/context7.yml deleted file mode 100644 index 168010bc4..000000000 --- a/.github/workflows/context7.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Refresh Context7 library - -on: - push: - branches: - - main - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - context7: - name: Refresh Context7 library - runs-on: ubuntu-latest - steps: - - - name: refresh context7 library - run: | - curl -X POST https://context7.com/api/refresh-library \ - -H "Content-Type: application/json" \ - -d '{"libraryName": "/arcadeai/docs"}' - - diff --git a/.github/workflows/generate-toolkit-docs.md b/.github/workflows/generate-toolkit-docs.md index aa0eb8a48..13181d04a 100644 --- a/.github/workflows/generate-toolkit-docs.md +++ b/.github/workflows/generate-toolkit-docs.md @@ -7,13 +7,15 @@ This workflow regenerates toolkit JSON and opens a PR with the changes. It can b 1. Builds the toolkit docs generator. 2. Generates toolkit JSON in `toolkit-docs-generator/data/toolkits` using the experience API's public tool catalog, which serves tool definitions and toolkit branding in one anonymous read. 3. Syncs integrations sidebar navigation from the generated JSON. -4. Creates or updates a PR on the stable `automation/toolkit-docs` branch if any files changed. Later runs overwrite that open PR with the latest generated docs. +4. Regenerates `public/llms.txt` so the automation PR does not depend on a second workflow. +5. Creates or updates a PR on the stable `automation/toolkit-docs` branch if any files changed. Later runs overwrite that open PR with the latest generated docs. ## Inputs and secrets Required secrets: - `ANTHROPIC_API_KEY` +- `OPENAI_API_KEY` (for `llms.txt` summaries) Optional secrets: diff --git a/.github/workflows/generate-toolkit-docs.yml b/.github/workflows/generate-toolkit-docs.yml index 000e6e06f..799e92987 100644 --- a/.github/workflows/generate-toolkit-docs.yml +++ b/.github/workflows/generate-toolkit-docs.yml @@ -3,12 +3,21 @@ name: Generate toolkit docs # Flow: # 1) Generate toolkit JSON into toolkit-docs-generator/data/toolkits # 2) Sync integrations sidebar _meta.tsx from toolkit-docs-generator/data/toolkits -# 3) Create or update a PR if changes were produced +# 3) Regenerate public/llms.txt so the automation PR is self-contained +# 4) Create or update a PR if changes were produced +# +# Opens the PR with GITHUB_TOKEN. That does not start other workflows, so this +# job generates llms.txt here instead of depending on llmstxt.yml to fire. on: repository_dispatch: types: [porter_deploy_succeeded] workflow_dispatch: + inputs: + public_catalog_url: + description: "Override public catalog URL (staging only; production is the default)" + required: false + type: string # 11:00 UTC = 3 AM PST / 4 AM PDT — late enough that DST drift doesn't matter. schedule: - cron: "0 11 * * *" @@ -67,11 +76,11 @@ jobs: --verbose \ --api-source public-catalog \ --llm-provider anthropic \ - --llm-model "$ANTHROPIC_MODEL" \ + --llm-model "claude-sonnet-4-6" \ --llm-api-key "$ANTHROPIC_API_KEY" \ --llm-max-tokens 8192 \ --llm-editor-provider anthropic \ - --llm-editor-model "$ANTHROPIC_EDITOR_MODEL" \ + --llm-editor-model "claude-sonnet-4-6" \ --llm-editor-api-key "$ANTHROPIC_API_KEY" \ --toolkit-concurrency 8 \ --llm-concurrency 15 \ @@ -84,25 +93,20 @@ jobs: working-directory: toolkit-docs-generator env: # Unset in normal runs: the generator defaults to the production - # experience API, which needs no credentials. Set the secret to - # point a run at staging or a local BFF. - PUBLIC_CATALOG_URL: ${{ secrets.PUBLIC_CATALOG_URL }} + # experience API. Pass public_catalog_url on workflow_dispatch to + # point a manual run at staging or a local BFF. + PUBLIC_CATALOG_URL: ${{ inputs.public_catalog_url }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - ANTHROPIC_MODEL: ${{ secrets.ANTHROPIC_MODEL || 'claude-sonnet-4-6' }} - # Stronger model for the secret-coherence editor. Keeps - # stale-secret cleanup precise instead of re-summarizing the whole - # artifact. - ANTHROPIC_EDITOR_MODEL: ${{ secrets.ANTHROPIC_EDITOR_MODEL || 'claude-sonnet-4-6' }} - name: Sync toolkit sidebar navigation run: pnpm exec tsx toolkit-docs-generator/scripts/sync-toolkit-sidebar.ts --remove-empty-sections=false --verbose - - name: Get app token - id: app-token - uses: ./.github/actions/app-token - with: - app-id: ${{ secrets.DOCS_BOT_CLIENT_ID }} - private-key: ${{ secrets.DOCS_BOT_PRIVATE_KEY }} + # GITHUB_TOKEN PRs do not start llmstxt.yml, so generate the file here. + # The toolkit generator CLI still does not write llms.txt. + - name: Generate llms.txt + run: pnpm llmstxt + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Create pull request id: cpr @@ -110,9 +114,7 @@ jobs: env: HUSKY: 0 with: - token: ${{ steps.app-token.outputs.token }} - author: ${{ steps.app-token.outputs.committer }} - committer: ${{ steps.app-token.outputs.committer }} + token: ${{ github.token }} commit-message: "[AUTO] Adding MCP Servers docs update" title: "[AUTO] Adding MCP Servers docs update" body: | @@ -139,7 +141,7 @@ jobs: echo "::warning::Could not request review on PR #${{ steps.cpr.outputs.pull-request-number }}: $(cat review-error.log)" fi env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_TOKEN: ${{ github.token }} - name: Upload generation report if: always() @@ -222,7 +224,7 @@ jobs: if [ "$GENERATION_SUCCEEDED" = "true" ]; then headline=":warning: Toolkit docs generated, but the workflow failed afterward" - detail="Generation succeeded, so the toolkit JSON is fine. A later workflow step failed — inspect the run to determine whether sidebar sync, PR creation, artifact upload, or Slack notification needs attention." + detail="Generation succeeded, so the toolkit JSON is fine. A later workflow step failed — inspect the run to determine whether sidebar sync, llms.txt generation, PR creation, artifact upload, or Slack notification needs attention." else headline=":rotating_light: Toolkit docs generation failed" detail="The failed run contains the exact file path and validation error." diff --git a/.github/workflows/llmstxt.yml b/.github/workflows/llmstxt.yml index 3ff948187..7564d7424 100644 --- a/.github/workflows/llmstxt.yml +++ b/.github/workflows/llmstxt.yml @@ -27,10 +27,6 @@ jobs: name: Generate LLMSTXT runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - name: Use Node.js uses: actions/setup-node@v4 @@ -41,10 +37,6 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref || github.ref }} - # No credentials to persist: the app token is minted further down, - # once there is something to push. The commit step below supplies it - # to git explicitly. - persist-credentials: false - name: Install dependencies run: npm install -g pnpm @@ -66,36 +58,26 @@ jobs: echo "has_changes=false" >> $GITHUB_OUTPUT fi - - name: Get app token - if: steps.check-changes.outputs.has_changes == 'true' - id: app-token - uses: ./.github/actions/app-token - with: - app-id: ${{ secrets.DOCS_BOT_CLIENT_ID }} - private-key: ${{ secrets.DOCS_BOT_PRIVATE_KEY }} - + # GITHUB_TOKEN pushes do not start other workflows. That is acceptable + # here: Test already ran on the author's commit, and llms.txt is a + # generated artifact. - name: Commit changes to PR if: steps.check-changes.outputs.has_changes == 'true' && github.event_name == 'pull_request' env: - GH_APP_TOKEN: ${{ steps.app-token.outputs.token }} - USER_NAME: ${{ steps.app-token.outputs.user-name }} - USER_EMAIL: ${{ steps.app-token.outputs.user-email }} HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | - git config user.name "$USER_NAME" - git config user.email "$USER_EMAIL" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add public/llms.txt git commit -m "🤖 Regenerate LLMs.txt" - git push "https://x-access-token:${GH_APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${HEAD_REF}" + git push origin "HEAD:${HEAD_REF}" - name: Create Pull Request (for scheduled/manual runs) if: steps.check-changes.outputs.has_changes == 'true' && github.event_name != 'pull_request' id: cpr uses: peter-evans/create-pull-request@v7 with: - token: ${{ steps.app-token.outputs.token }} - author: ${{ steps.app-token.outputs.committer }} - committer: ${{ steps.app-token.outputs.committer }} + token: ${{ github.token }} commit-message: Regenerate LLMs.txt and related files branch: auto-update-llms-txt delete-branch: true @@ -109,10 +91,11 @@ jobs: continue-on-error: true run: gh pr edit ${{ steps.cpr.outputs.pull-request-number }} --add-reviewer ArcadeAI/engineering-tools-and-dx env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_TOKEN: ${{ github.token }} - name: Enable Pull Request Automerge if: steps.check-changes.outputs.has_changes == 'true' && github.event_name != 'pull_request' && steps.cpr.outputs.pull-request-number != '' + continue-on-error: true run: gh pr merge --squash --auto ${{ steps.cpr.outputs.pull-request-number }} env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/translate-docs.yml b/.github/workflows/translate-docs.yml deleted file mode 100644 index 9741fc949..000000000 --- a/.github/workflows/translate-docs.yml +++ /dev/null @@ -1,298 +0,0 @@ -name: 📖 Translate Documentation - -on: - workflow_dispatch: - inputs: - target_locale: - description: "Target locale to translate" - required: false - default: "all" - type: choice - options: - - "all" - - "es" - - "pt-BR" - - force_translate: - description: "Force translate all files (ignore cache)" - required: false - default: false - type: boolean - - cleanup_deleted: - description: "Delete translated files when English originals are removed" - required: false - default: true - type: boolean - - dry_run: - description: "Dry run (don't write files or update cache)" - required: false - default: false - type: boolean - - single_file: - description: "Single file to translate (relative to app/en, e.g. 'resources/examples/page.mdx')" - required: false - default: "" - type: string - - concurrency: - description: "Number of files to translate in parallel (1-10)" - required: false - default: 3 - type: number - -jobs: - translate: - runs-on: ubuntu-latest - - steps: - - name: 🛒 Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: 📦 Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "pnpm" - - - name: 📥 Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: 📚 Install dependencies - run: pnpm install --frozen-lockfile - - - name: 🧹 Clean up deleted files - if: inputs.cleanup_deleted - run: | - echo "🔍 Checking for deleted English files..." - pnpm exec tsx scripts/i18n-sync/index.ts --cleanup - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - - - name: 🌍 Run translation - run: | - echo "🚀 Starting translation process..." - - # Build the command with dynamic inputs - CMD="pnpm exec tsx scripts/i18n-sync/index.ts" - - # Add target locale if not 'all' - if [ "${{ inputs.target_locale }}" != "all" ]; then - CMD="$CMD --locale ${{ inputs.target_locale }}" - fi - - # Add force flag if enabled - if [ "${{ inputs.force_translate }}" = "true" ]; then - CMD="$CMD --force" - fi - - # Add dry-run flag if enabled - if [ "${{ inputs.dry_run }}" = "true" ]; then - CMD="$CMD --dry-run" - fi - - # Add single file if specified - if [ -n "${{ inputs.single_file }}" ]; then - CMD="$CMD --file '${{ inputs.single_file }}'" - fi - - # Add concurrency - CMD="$CMD --concurrency ${{ inputs.concurrency }}" - - echo "💻 Executing: $CMD" - eval $CMD - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENAI_MODEL: ${{ secrets.OPENAI_MODEL || 'gpt-4o-mini' }} - - - name: 📊 Check translation status - id: check_changes - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "No changes detected" - echo "has_changes=false" >> $GITHUB_OUTPUT - else - echo "Changes detected" - echo "has_changes=true" >> $GITHUB_OUTPUT - - # Count changed files - CHANGED_FILES=$(git diff --name-only | wc -l) - STAGED_FILES=$(git diff --cached --name-only | wc -l) - TOTAL_CHANGES=$((CHANGED_FILES + STAGED_FILES)) - - echo "changed_files=$TOTAL_CHANGES" >> $GITHUB_OUTPUT - - # Get list of changed locales - CHANGED_LOCALES=$(git diff --name-only | grep -E '^app/(es|pt-BR)/' | cut -d'/' -f2 | sort -u | tr '\n' ',' | sed 's/,$//') - if [ -z "$CHANGED_LOCALES" ]; then - CHANGED_LOCALES=$(git diff --cached --name-only | grep -E '^app/(es|pt-BR)/' | cut -d'/' -f2 | sort -u | tr '\n' ',' | sed 's/,$//') - fi - echo "changed_locales=$CHANGED_LOCALES" >> $GITHUB_OUTPUT - fi - - - name: 🏷️ Generate branch name - if: steps.check_changes.outputs.has_changes == 'true' - id: branch_name - run: | - TIMESTAMP=$(date +%Y%m%d-%H%M%S) - if [ "${{ inputs.target_locale }}" != "all" ]; then - BRANCH_NAME="translations/${{ inputs.target_locale }}-$TIMESTAMP" - else - BRANCH_NAME="translations/all-locales-$TIMESTAMP" - fi - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - - name: 🌿 Create and switch to new branch - if: steps.check_changes.outputs.has_changes == 'true' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b ${{ steps.branch_name.outputs.branch_name }} - - - name: 📝 Commit changes - if: steps.check_changes.outputs.has_changes == 'true' - run: | - git add . - - # Create detailed commit message - COMMIT_MSG="🌍 Update translations" - - if [ "${{ inputs.target_locale }}" != "all" ]; then - COMMIT_MSG="$COMMIT_MSG for ${{ inputs.target_locale }}" - fi - - COMMIT_MSG="$COMMIT_MSG (${{ steps.check_changes.outputs.changed_files }} files)" - - if [ "${{ inputs.force_translate }}" = "true" ]; then - COMMIT_MSG="$COMMIT_MSG [forced]" - fi - - if [ "${{ inputs.cleanup_deleted }}" = "true" ]; then - COMMIT_MSG="$COMMIT_MSG [with cleanup]" - fi - - if [ "${{ inputs.dry_run }}" = "true" ]; then - COMMIT_MSG="$COMMIT_MSG [dry-run]" - fi - - # Add configuration details to commit body - COMMIT_BODY="Translation Configuration: - - Target Locale: ${{ inputs.target_locale }} - - Force Translate: ${{ inputs.force_translate }} - - Cleanup Deleted: ${{ inputs.cleanup_deleted }} - - Dry Run: ${{ inputs.dry_run }} - - Single File: ${{ inputs.single_file || 'none' }} - - Concurrency: ${{ inputs.concurrency }} - - Changed Locales: ${{ steps.check_changes.outputs.changed_locales }} - - Generated by GitHub Actions workflow" - - git commit -m "$COMMIT_MSG" -m "$COMMIT_BODY" - - - name: 📤 Push branch - if: steps.check_changes.outputs.has_changes == 'true' - run: | - git push origin ${{ steps.branch_name.outputs.branch_name }} - - - name: 🔧 Create Pull Request - if: steps.check_changes.outputs.has_changes == 'true' - uses: actions/github-script@v7 - with: - script: | - const { data: pr } = await github.rest.pulls.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: `🌍 Translation Update: ${{ inputs.target_locale }} (${{ steps.check_changes.outputs.changed_files }} files)`, - head: '${{ steps.branch_name.outputs.branch_name }}', - base: 'main', - body: `## 📖 Automated Translation Update - - This PR contains automated translations generated by the GitHub Actions workflow. - - ### 📊 Translation Summary - - **Target Locale**: ${{ inputs.target_locale }} - - **Files Changed**: ${{ steps.check_changes.outputs.changed_files }} - - **Locales Updated**: ${{ steps.check_changes.outputs.changed_locales }} - - ### ⚙️ Configuration Used - - **Force Translate**: ${{ inputs.force_translate }} - - **Cleanup Deleted Files**: ${{ inputs.cleanup_deleted }} - - **Dry Run**: ${{ inputs.dry_run }} - - **Single File**: ${{ inputs.single_file || 'None (all files)' }} - - **Concurrency**: ${{ inputs.concurrency }} - - **Model**: gpt-4o-mini - - ### 🔍 Review Guidelines - Please review the translations for: - - [ ] Accuracy and context preservation - - [ ] Proper handling of technical terms - - [ ] UI/Dashboard elements remain in English - - [ ] Code blocks and inline code unchanged - - [ ] Markdown formatting preserved - - [ ] Brand names (Arcade, Arcade Engine, Control Plane) kept in English - - ### 🚀 Auto-generated - This PR was automatically created by the \`translate-docs.yml\` GitHub Action. - - **Triggered by**: @${{ github.actor }} - **Workflow Run**: [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`, - draft: false - }); - - // Add labels - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: ['🌍 translation', '🤖 automated', 'documentation'] - }); - - // Add specific locale label if not 'all' - if ('${{ inputs.target_locale }}' !== 'all') { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: [`locale:${{ inputs.target_locale }}`] - }); - } - - console.log(`Created PR #${pr.number}: ${pr.html_url}`); - - - name: 📄 Summary - if: always() - run: | - echo "## 🌍 Translation Workflow Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ steps.check_changes.outputs.has_changes }}" = "true" ]; then - echo "✅ **Translation completed successfully!**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Files changed**: ${{ steps.check_changes.outputs.changed_files }}" >> $GITHUB_STEP_SUMMARY - echo "- **Target locale**: ${{ inputs.target_locale }}" >> $GITHUB_STEP_SUMMARY - echo "- **Branch created**: \`${{ steps.branch_name.outputs.branch_name }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Changed locales**: ${{ steps.check_changes.outputs.changed_locales }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "A Pull Request has been created for review." >> $GITHUB_STEP_SUMMARY - else - echo "ℹ️ **No changes detected**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "All translations are up to date. No PR was created." >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Configuration Used" >> $GITHUB_STEP_SUMMARY - echo "- Target Locale: ${{ inputs.target_locale }}" >> $GITHUB_STEP_SUMMARY - echo "- Force Translate: ${{ inputs.force_translate }}" >> $GITHUB_STEP_SUMMARY - echo "- Cleanup Deleted: ${{ inputs.cleanup_deleted }}" >> $GITHUB_STEP_SUMMARY - echo "- Dry Run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY - echo "- Single File: ${{ inputs.single_file || 'None' }}" >> $GITHUB_STEP_SUMMARY - echo "- Concurrency: ${{ inputs.concurrency }}" >> $GITHUB_STEP_SUMMARY - echo "- Model: gpt-4o-mini" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/update-design-system-dependency.yml b/.github/workflows/update-design-system-dependency.yml index 50f33522b..21627db5d 100644 --- a/.github/workflows/update-design-system-dependency.yml +++ b/.github/workflows/update-design-system-dependency.yml @@ -47,14 +47,8 @@ jobs: pnpm vitest run toolkit-docs-generator/tests/scripts/sync-toolkit-sidebar.test.ts toolkit-docs-generator/tests/sources/oauth-provider-resolver.test.ts pnpm run build - - name: Get app token - if: steps.check-changes.outputs.has_changes == 'true' - id: app-token - uses: ./.github/actions/app-token - with: - app-id: ${{ secrets.DOCS_BOT_CLIENT_ID }} - private-key: ${{ secrets.DOCS_BOT_PRIVATE_KEY }} - + # Compatibility tests and a production build run above, so this workflow + # does not depend on Test starting on the opened PR. - name: Create pull request if: steps.check-changes.outputs.has_changes == 'true' id: cpr @@ -63,9 +57,7 @@ jobs: HUSKY: 0 SKIP_HUSKY: 1 with: - token: ${{ steps.app-token.outputs.token }} - author: ${{ steps.app-token.outputs.committer }} - committer: ${{ steps.app-token.outputs.committer }} + token: ${{ github.token }} commit-message: "chore: update @arcadeai/design-system to latest" title: "chore: update @arcadeai/design-system to latest" body: | @@ -87,4 +79,4 @@ jobs: continue-on-error: true run: gh pr edit ${{ steps.cpr.outputs.pull-request-number }} --add-reviewer ArcadeAI/engineering-tools-and-dx env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 33d97c1d2..fe5b7ed54 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ Then, run `pnpm dev` to start the development server and visit localhost:3000. ### Environment Variables -Copy `.env.example` to `.env.local` and fill in the values: +Copy `.env.local.example` to `.env.local` and fill in the values: ```bash -cp .env.example .env.local +cp .env.local.example .env.local ``` | Variable | Required | Purpose | diff --git a/_dictionaries/es.ts b/_dictionaries/es.ts deleted file mode 100644 index 2c48bbf73..000000000 --- a/_dictionaries/es.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Dictionary } from "./i18n-config"; - -export default { - dark: "Oscuro", - light: "Luz", - system: "Sistema", - toc: { - title: "En esta página", - backToTop: "Desplazarse hacia arriba", - }, - lastUpdated: "Última actualización el", - notFound: "Esta página no se pudo encontrar", - poweredBy: "Desarrollado por", - editPage: "Edite esta página en GitHub →", - by: "por", - banner: { - aiTranslation: - "🤖 Esta traducción está en progreso y fue generada por IA. Si encuentras algo incorrecto o quieres ayudar a mejorarla, por favor", - contributeLink: "contribuye en GitHub", - thanks: "¡Gracias por tu ayuda!", - }, - notFoundPage: { - title: "Página no encontrada", - description: "Esta página no existe o puede haber sido movida.", - notAvailablePrefix: "No disponible en", - tryEnglish: "Probar versión en inglés", - translationHint: "Es posible que esta página aún no esté traducida.", - viewOriginalEnglish: "Ver el original en inglés", - goHome: "Ir a la página principal", - goBack: "Volver", - needHelp: "¿Necesitas ayuda? Prueba estas páginas populares:", - quickstart: "Inicio rápido", - mcpServers: "MCP Servers", - createMcpServer: "Crear un MCP Server", - }, -} satisfies Dictionary; diff --git a/_dictionaries/get-dictionary-client.ts b/_dictionaries/get-dictionary-client.ts deleted file mode 100644 index 15b5bd1f9..000000000 --- a/_dictionaries/get-dictionary-client.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Dictionaries, Dictionary, Locale } from "./i18n-config"; - -// Client-safe dictionary loader (without server-only restriction) -const dictionaries: Dictionaries = { - en: () => import("./en"), - es: () => import("./es"), - "pt-BR": () => import("./pt-BR"), -}; - -export async function getDictionaryClient(locale: string): Promise { - const localeKey: Locale = (Object.keys(dictionaries) as Locale[]).includes( - locale as Locale - ) - ? (locale as Locale) - : "en"; - const { default: dictionary } = await dictionaries[localeKey](); - - return dictionary; -} diff --git a/_dictionaries/get-dictionary.ts b/_dictionaries/get-dictionary.ts index 30ca47f95..6ef9aaa7f 100644 --- a/_dictionaries/get-dictionary.ts +++ b/_dictionaries/get-dictionary.ts @@ -1,21 +1,7 @@ import "server-only"; -import type { Dictionaries, Dictionary, Locale } from "./i18n-config"; - -// We enumerate all dictionaries here for better linting and TypeScript support -// We also get the default import for cleaner types -const dictionaries: Dictionaries = { - en: () => import("./en"), - es: () => import("./es"), - "pt-BR": () => import("./pt-BR"), -}; - -export async function getDictionary(locale: string): Promise { - const localeKey: Locale = (Object.keys(dictionaries) as Locale[]).includes( - locale as Locale - ) - ? (locale as Locale) - : "en"; - const { default: dictionary } = await dictionaries[localeKey](); +import type { Dictionary } from "./i18n-config"; +export async function getDictionary(_locale?: string): Promise { + const { default: dictionary } = await import("./en"); return dictionary; } diff --git a/_dictionaries/i18n-config.ts b/_dictionaries/i18n-config.ts index 82ab483f7..2ec8afc92 100644 --- a/_dictionaries/i18n-config.ts +++ b/_dictionaries/i18n-config.ts @@ -2,14 +2,9 @@ import type EnglishLocale from "./en"; export const i18n = { defaultLocale: "en", - locales: ["en", "es", "pt-BR"], + locales: ["en"], } as const; export type Locale = (typeof i18n)["locales"][number]; export type Dictionary = typeof EnglishLocale; - -export type Dictionaries = Record< - Locale, - () => Promise<{ default: Dictionary }> ->; diff --git a/_dictionaries/pt-BR.ts b/_dictionaries/pt-BR.ts deleted file mode 100644 index 3d3e42c88..000000000 --- a/_dictionaries/pt-BR.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Dictionary } from "./i18n-config"; - -export default { - dark: "Escuro", - light: "Claro", - system: "Sistema", - toc: { - title: "Nesta página", - backToTop: "Voltar ao topo", - }, - lastUpdated: "Última atualização em", - notFound: "Esta página não pôde ser encontrada", - poweredBy: "Desenvolvido por", - editPage: "Edite esta página no GitHub →", - by: "por", - banner: { - aiTranslation: - "🤖 Esta tradução está em andamento e foi gerada por IA. Se você encontrar algo incorreto ou quiser ajudar a melhorá-la, por favor", - contributeLink: "contribua no GitHub", - thanks: "Obrigado pela sua ajuda!", - }, - notFoundPage: { - title: "Página não encontrada", - description: "Esta página não existe ou pode ter sido movida.", - notAvailablePrefix: "Não disponível em", - tryEnglish: "Tentar versão em inglês", - translationHint: "Esta página pode ainda não estar traduzida.", - viewOriginalEnglish: "Ver o original em inglês", - goHome: "Ir para a página inicial", - goBack: "Voltar", - needHelp: "Precisa de ajuda? Experimente estas páginas populares:", - quickstart: "Início rápido", - mcpServers: "MCP Servers", - createMcpServer: "Criar um MCP Server", - }, -} satisfies Dictionary; diff --git a/app/_components/contact-email.tsx b/app/_components/contact-email.tsx index 075b31297..4896ca845 100644 --- a/app/_components/contact-email.tsx +++ b/app/_components/contact-email.tsx @@ -4,10 +4,7 @@ import Link from "next/link"; import type { ReactNode } from "react"; import { useEffect, useState } from "react"; -// TODO(i18n): hardcoded to the English contact page. Docs content is en-only -// today, so this is the safe target (a // link could 404 for locales -// without the page). When translated content ships, derive the active locale -// (e.g. via usePathname) and point at //resources/contact-us. +// Docs are English-only under /en. const CONTACT_PAGE = "/en/resources/contact-us"; const linkClassName = diff --git a/app/_components/translation-banner.tsx b/app/_components/translation-banner.tsx deleted file mode 100644 index 4dcdb884f..000000000 --- a/app/_components/translation-banner.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { cn } from "@arcadeai/design-system/lib/utils"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { Banner } from "nextra/components"; - -type TranslationBannerProps = { - dictionary: { - banner: { - aiTranslation: string; - contributeLink: string; - thanks: string; - }; - }; - locale: string; -}; - -/** - * Constructs the GitHub edit URL for the current page - * Example: /es/home/auth -> https://github.com/ArcadeAI/docs/blob/main/app/es/home/auth/page.mdx - */ -function constructGithubUrl(pathname: string, locale: string): string { - const baseUrl = "https://github.com/ArcadeAI/docs/blob/main/app"; - - // Remove leading slash if present - const cleanPath = pathname.startsWith("/") ? pathname.slice(1) : pathname; - - // If the path doesn't start with the locale, add it - const pathWithLocale = cleanPath.startsWith(`${locale}/`) - ? cleanPath - : `${locale}/${cleanPath}`; - - // Construct the full GitHub URL pointing to the page.mdx file - return `${baseUrl}/${pathWithLocale}/page.mdx`; -} - -export function TranslationBanner({ - dictionary, - locale, -}: TranslationBannerProps) { - const pathname = usePathname(); - const githubUrl = constructGithubUrl(pathname || "/", locale); - - return ( - -
- {dictionary.banner.aiTranslation} - - {dictionary.banner.contributeLink} - - . {dictionary.banner.thanks} -
-
- ); -} diff --git a/app/en/get-started/mcp-clients/claude-desktop/page.mdx b/app/en/get-started/mcp-clients/claude-desktop/page.mdx index 827c84356..9deda727c 100644 --- a/app/en/get-started/mcp-clients/claude-desktop/page.mdx +++ b/app/en/get-started/mcp-clients/claude-desktop/page.mdx @@ -15,6 +15,8 @@ export const STEP_3_DARK_WIDTH = 1104; export const STEP_3_DARK_HEIGHT = 874; export const STEP_3_LIGHT_WIDTH = 1094; export const STEP_3_LIGHT_HEIGHT = 882; +export const AUTH_OPTIONS_WIDTH = 356; +export const AUTH_OPTIONS_HEIGHT = 701; export const STEP_4_DARK_WIDTH = 1860; export const STEP_4_DARK_HEIGHT = 206; export const STEP_4_LIGHT_WIDTH = 1826; @@ -101,7 +103,13 @@ On the settings page, click on the "Connectors" tab, and then on the "Add custom height={STEP_2_DARK_HEIGHT / IMAGE_SCALE_FACTOR} /> -A modal dialog will open asking you for a name and a URL. Enter a name for your connector, and the URL of your MCP Gateway. Then, click on the "Add" button. +A dialog opens for a name and a URL. Enter a name for your connector and your MCP Gateway URL: + +``` +https://api.arcade.dev/mcp/{YOUR-GATEWAY-SLUG} +``` + +If you self-host the Arcade Engine, use your Engine hostname in place of `api.arcade.dev`. Then continue. {"Step -### authenticate with your Arcade account +### Keep the detected defaults + +Claude checks the gateway URL and shows **Authentication** and **OAuth client** options. Keep the values marked **Detected**: + +- **Sign in now** for Authentication +- **Use Claude's published identity** for OAuth client + +Those defaults match Arcade Auth and [User Sources](/operate/identity/user-sources). Do not switch to **Sign in when needed** or **No sign-in**. Those options skip or delay the OAuth flow Arcade uses. Do not switch to **Register automatically** or **Use your own OAuth client**. Arcade already supports Claude's published identity (CIMD). Leave **Request headers** empty. Leave **Advanced** collapsed. + +Then click **Add**. + +{"Claude + + + If your dialog is a single screen with **Advanced settings** instead of these + options, leave Advanced settings empty and click **Add**. The result is the + same. + + On Claude Team and Enterprise plans, only an Owner can add a custom + connector. In **Organization settings → Connectors**, click **Add**, then + **Custom**, then **Web**. Paste the gateway URL, keep the Detected defaults, + and click **Add**. Each member then clicks **Connect** under **Customize → + Connectors**. + + +### Authenticate with your Arcade account You will see a new connector added to your list of connectors. Click on the "Connect" button to authenticate with your Arcade account. diff --git a/app/en/operate/deploy/architecture/page.mdx b/app/en/operate/deploy/architecture/page.mdx index e5b55b6e9..fcf328934 100644 --- a/app/en/operate/deploy/architecture/page.mdx +++ b/app/en/operate/deploy/architecture/page.mdx @@ -4,6 +4,7 @@ description: "The services that make up a self-hosted Arcade deployment and how --- import { Callout } from "nextra/components"; +import Image from "next/image"; # Platform architecture @@ -11,6 +12,44 @@ A self-hosted Arcade deployment runs the services below on your Kubernetes clust This page describes what each service does and what it connects to. For installation steps, see [Self-host with Helm](/operate/deploy/helm). +## Reference topology + +The diagram below shows a complete deployment: the Kubernetes clusters that run the platform and its workers, the network boundaries each call crosses, and the data tier behind every service. The sections that follow describe each piece. + +{/* + Source diagram, edit it here and re-export the PNG: + https://whimsical.com/arcade719/arcade-reference-topology-diagram-8DCV7aoMm9rmBkZg2xvpem + + The export has a baked-in white background, so the figure keeps a light + canvas in both themes rather than inverting. In dark mode the image is + dimmed to 83% and the panel behind it is set to neutral-300, the same value, + so the two match seamlessly and the figure reads as a lit panel instead of a + glaring rectangle. Brightness scales every channel equally, so the diagram's + own contrast is unchanged. The figure stays inside the content column. +*/} + +
+ + Arcade reference topology diagram: agents and tool developers reach the ingress gateway, which fronts the application plane Kubernetes cluster running the Coordinator, Engine, Usage, dashboard, Experience API, embedding and search services, and OpenFGA, alongside a worker plane cluster running Arcade workers and custom MCP servers; egress gateways reach identity providers, telemetry, container registries, and connected services; PostgreSQL, Redis, and blob storage sit in the data tier below. A key marks the tool calling hot path and the arcade deploy path + +
+ The Arcade reference topology. Open the diagram in a new tab to zoom in and read the labels. +
+
+ ## Services | Service | Responsible for | Talks to | Required | diff --git a/app/en/operate/governance/mcp-gateways/create-via-dashboard/page.mdx b/app/en/operate/governance/mcp-gateways/create-via-dashboard/page.mdx index 80bee2700..5db94f185 100644 --- a/app/en/operate/governance/mcp-gateways/create-via-dashboard/page.mdx +++ b/app/en/operate/governance/mcp-gateways/create-via-dashboard/page.mdx @@ -3,6 +3,7 @@ title: "Create via Dashboard" description: "Create and configure MCP Gateways using the Arcade dashboard" --- +import { Callout } from "nextra/components"; import Image from "next/image"; import { SignupLink } from "@/app/_components/analytics"; @@ -69,6 +70,23 @@ See [Skip consent for trusted MCP clients](/operate/governance/mcp-gateways#skip ## After Creating a Gateway -Once you've created a gateway, you'll need to add it to your chat client. The assistant will provide the MCP URL (for example, `https://api.arcade.dev/mcp/{YOUR-GATEWAY-SLUG}`). +Once you've created a gateway, add it to your MCP client. The dashboard shows the MCP URL (for example, `https://api.arcade.dev/mcp/{YOUR-GATEWAY-SLUG}`). See [Connect to MCP clients](/get-started/mcp-clients) for setup instructions specific to your client. + +### Point Claude at the gateway + +For Claude Desktop and Claude.ai, set the gateway to **Arcade Auth** or a **User Source**. Claude needs a browser OAuth flow, so **Arcade Headers** does not work here. + +Add the gateway URL as a custom connector. Claude shows **Authentication** and **OAuth client** options. Keep the values marked **Detected**: + +- **Sign in now** for Authentication +- **Use Claude's published identity** for OAuth client + +On Claude Team and Enterprise plans, only an Owner can add the connector. Members then click **Connect**. See [Keep the detected defaults](/get-started/mcp-clients/claude-desktop#keep-the-detected-defaults). + + + To skip Arcade's consent screen for Claude users, allowlist Claude's published + identity on the gateway. See [Skip consent for trusted MCP + clients](/operate/governance/mcp-gateways#skip-consent-for-trusted-mcp-clients). + diff --git a/app/en/operate/governance/mcp-gateways/page.mdx b/app/en/operate/governance/mcp-gateways/page.mdx index 9a21fa217..56fb58a77 100644 --- a/app/en/operate/governance/mcp-gateways/page.mdx +++ b/app/en/operate/governance/mcp-gateways/page.mdx @@ -89,6 +89,10 @@ The values below are the literal `client_id` strings these clients send in their | Claude Code | `https://claude.ai/oauth/claude-code-client-metadata` | | Visual Studio Code | `https://vscode.dev/oauth/client-metadata.json` | +When you add an Arcade gateway as a custom connector in Claude, keep **Use Claude's published identity**. That option sends the Claude CIMD URL from the preceding table. Allowlist the same URL if you want Claude users to skip Arcade's consent screen. Do not switch Claude to **Register automatically** if you want to skip the consent screen. Dynamic Client Registration does not produce a stable client ID. + +See [Keep the detected defaults](/get-started/mcp-clients/claude-desktop#keep-the-detected-defaults) for the rest of the Claude connector options. + ### Find the client ID for an unlisted client If the MCP client publishes a CIMD, its documentation will reference an `https://...client-metadata` URL or similar. That URL is the client ID, so paste it into the allowlist as-is. diff --git a/app/en/operate/governance/remote-mcp-servers/_meta.tsx b/app/en/operate/governance/remote-mcp-servers/_meta.tsx index 3f1e47d24..34ac5af52 100644 --- a/app/en/operate/governance/remote-mcp-servers/_meta.tsx +++ b/app/en/operate/governance/remote-mcp-servers/_meta.tsx @@ -28,6 +28,9 @@ const meta: MetaRecord = { splunk: { title: "Splunk", }, + aws: { + title: "AWS", + }, }; export default meta; diff --git a/app/en/operate/governance/remote-mcp-servers/aws/page.mdx b/app/en/operate/governance/remote-mcp-servers/aws/page.mdx new file mode 100644 index 000000000..0804c221d --- /dev/null +++ b/app/en/operate/governance/remote-mcp-servers/aws/page.mdx @@ -0,0 +1,116 @@ +--- +title: "Connect the AWS MCP Server" +description: "Connect Arcade to the AWS-managed MCP Server using AWS Sign-In OAuth, with access governed entirely by IAM" +--- + +import { Callout, Steps } from "nextra/components"; +import { SignupLink } from "@/app/_components/analytics"; + +# Connect the AWS MCP Server + +AWS hosts a managed **AWS MCP Server** that gives agents access to AWS APIs across thousands of operations, authorized through **AWS Sign-In** using the same identities, permissions, and governance model you already use with the AWS Management Console and CLI. This guide covers the Arcade-side setup for connecting it as a [remote MCP server](/operate/governance/remote-mcp-servers), plus the AWS settings that most commonly trip people up. + + + This guide is about connecting to the AWS MCP Server, not an Arcade + toolkit. Arcade doesn't ship an AWS toolkit today, so this remote server + is currently the only way to reach AWS APIs through Arcade. It's a fixed, + AWS-defined tool set, not something a customer can extend, so that + advantage goes away if Arcade ships a native toolkit for it later. + + + + The AWS-side steps below are sourced directly from [AWS's own OAuth + documentation](https://docs.aws.amazon.com/signin/latest/userguide/aws-mcp-server.html) + for this server. + + +AWS's setup is structurally closer to Atlassian than to the other guides in this section: the AWS MCP Server uses **OAuth 2.1 with Dynamic Client Registration (DCR)**, so Arcade registers itself as a client automatically. There's no manual OAuth app to create and no Client ID/Secret to copy. The administrative work is entirely about **which IAM identity authorizes the connection and what that identity is permitted to do**, because authorizing an agent grants it no additional AWS permissions. It acts strictly within the authorizing identity's existing IAM policies. + + + + +Connect the AWS MCP Server to Arcade and use its tools in gateways and SDKs. + + + + + +- An Arcade account +- An AWS account, and an IAM identity (user or role) you'll authorize the connection as +- Permission to attach an IAM policy to that identity + + + + + +- Which IAM permissions the authorizing identity needs, and why access is governed entirely by IAM +- Register the remote server in Arcade without manually configuring OAuth credentials +- Diagnose the most common setup mistakes from their error messages + + + + +## Set up AWS + +Because access runs entirely through the authorizing identity's IAM permissions, setting up AWS mainly means granting that identity the permissions to complete the OAuth flow. + +- **Grant the authorizing identity the OAuth permissions.** Attach the AWS managed policy [`AWSMCPSignInOAuthAccessPolicy`](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSMCPSignInOAuthAccessPolicy.html) to the IAM user or role you'll sign in as: + + ``` + aws iam attach-role-policy \ + --role-name MyRole \ + --policy-arn arn:aws:iam::aws:policy/AWSMCPSignInOAuthAccessPolicy + ``` + + This grants the two permissions the interactive OAuth flow needs: `signin:AuthorizeOAuth2Access` and `signin:CreateOAuth2Token`. (If you authorize as the account root user, no extra IAM permissions are required, but a scoped role is the better practice.) + +- **Understand what the authorized connection can actually do.** These two permissions only let the identity complete the OAuth handshake. They don't grant any AWS service access. Every tool call the AWS MCP Server makes afterward runs under the authorizing identity's own IAM policies. Whatever that identity can't do in the Console or CLI, it can't do through the MCP server either. Scope the authorizing role to exactly the AWS access you intend agents to have, since that role's policies *are* the access-control boundary for everything Arcade does here. + +- **Pick the region deliberately.** The AWS MCP Server endpoint and its OAuth resources are region-specific, and the server is available in a limited set of regions while AWS rolls it out. Use a region where the endpoint actually resolves, and keep it consistent between the server URL and the authorizing session. + + + All OAuth activity (authorization requests, token issuance, + introspection, revocation, along with the OAuth client, redirect URI, and + originating sign-in session) is recorded in **AWS CloudTrail**, so + administrators can correlate downstream AWS API calls back to the + originating authorization. This is worth knowing for audit and incident + response. + + +## Configure the remote server in Arcade + + + +### Register the server + +Go to the [MCP servers dashboard](https://app.arcade.dev/servers), click **Add Server**, choose **Remote MCP**, and enter a server ID and the AWS MCP Server endpoint for your region: + +``` +https://aws-mcp..api.aws/mcp +``` + +For example, `https://aws-mcp.us-east-1.api.aws/mcp`. The region in the URL should match the region of the identity and session you authorize with. If the hostname doesn't resolve, that region isn't served yet: pick one that is, rather than treating it as a connection or permissions problem. + +### Configure OAuth2 authorization + +Open **Advanced settings → OAuth2 authorization** and **leave Client ID, Client Secret, Authorization URL, and Token URL empty.** This is the same guidance as Atlassian, and the opposite of the ECA-style providers: the AWS MCP Server supports Dynamic Client Registration, so Arcade registers a client and discovers the authorization server automatically on first connection. There's nothing to create on the AWS side to populate these fields, and there's no redirect URI to manually configure on an AWS OAuth app. The redirect URI is matched against AWS's DCR allowlist, not a per-account setting. + +### Authorize and confirm + +Save the server to open the authorization prompt. Sign in through AWS Sign-In as the IAM identity you scoped above and approve the authorization request. AWS Sign-In issues an access token (valid up to one hour, auto-refreshed via a rotating single-use refresh token while your session lasts), and the connection lists the tools that identity is permitted to reach. + + + +## Troubleshooting + +- **"Agent cannot register as an OAuth client" during connection**: AWS Sign-In only permits Dynamic Client Registration from approved redirect URIs, and Arcade's is approved. This isn't something you fix through Arcade configuration. It's resolved on AWS's side. +- **"Permission denied" when connecting**: the authorizing IAM identity is missing `signin:AuthorizeOAuth2Access` and/or `signin:CreateOAuth2Token`. Attach `AWSMCPSignInOAuthAccessPolicy` to that identity. +- **The connection authorizes, but specific tool calls fail with access-denied errors**: the authorizing identity's own IAM policies don't permit that AWS action. This isn't an MCP or Arcade issue: authorizing the agent grants no permissions beyond what the identity already has. Broaden the role's IAM policies (deliberately) or re-authorize as an identity with the needed access. +- **"Invalid target resource"**: the endpoint region or resource is off. Confirm the server URL uses the `https://aws-mcp..api.aws/mcp` pattern with a supported region. +- **Access stops working after about an hour, and doesn't recover**: the refresh token has expired or been revoked. Re-authorize through the AWS Sign-In flow to obtain a fresh token. +- **You need to immediately cut off an authorized connection**: revoking the refresh token stops new access tokens from being issued, but existing access tokens remain valid until they expire (up to an hour). For immediate containment, apply an IAM policy using the `aws:SignInSessionArn` condition key to deny requests tied to that sign-in session. + +## Next steps + +- [Create an MCP Gateway](/operate/governance/mcp-gateways/create-via-dashboard) to expose this server's tools. +- [Connect to MCP clients](/get-started/mcp-clients). diff --git a/app/en/operate/governance/remote-mcp-servers/page.mdx b/app/en/operate/governance/remote-mcp-servers/page.mdx index 22b9e1cc0..7987eefc3 100644 --- a/app/en/operate/governance/remote-mcp-servers/page.mdx +++ b/app/en/operate/governance/remote-mcp-servers/page.mdx @@ -175,7 +175,7 @@ Common settings include: height={ADVANCED_SETTINGS_HEIGHT} /> -Some remote servers need provider-specific setup beyond these generic settings. See [Connect a Salesforce Hosted MCP Server](/operate/governance/remote-mcp-servers/salesforce), [Connect a ServiceNow Hosted MCP Server](/operate/governance/remote-mcp-servers/servicenow), [Connect a Dynamics 365 Customer Service MCP Server](/operate/governance/remote-mcp-servers/dynamics-365-customer-service), [Connect a HubSpot Remote MCP Server](/operate/governance/remote-mcp-servers/hubspot), [Connect a Snowflake-Managed MCP Server](/operate/governance/remote-mcp-servers/snowflake), [Connect an Atlassian Remote MCP Server](/operate/governance/remote-mcp-servers/atlassian), [Connect a GitHub Remote MCP Server](/operate/governance/remote-mcp-servers/github), or [Connect a Splunk MCP Server](/operate/governance/remote-mcp-servers/splunk) for fully worked examples. +Some remote servers need provider-specific setup beyond these generic settings. See [Connect a Salesforce Hosted MCP Server](/operate/governance/remote-mcp-servers/salesforce), [Connect a ServiceNow Hosted MCP Server](/operate/governance/remote-mcp-servers/servicenow), [Connect a Dynamics 365 Customer Service MCP Server](/operate/governance/remote-mcp-servers/dynamics-365-customer-service), [Connect a HubSpot Remote MCP Server](/operate/governance/remote-mcp-servers/hubspot), [Connect a Snowflake-Managed MCP Server](/operate/governance/remote-mcp-servers/snowflake), [Connect an Atlassian Remote MCP Server](/operate/governance/remote-mcp-servers/atlassian), [Connect a GitHub Remote MCP Server](/operate/governance/remote-mcp-servers/github), [Connect a Splunk MCP Server](/operate/governance/remote-mcp-servers/splunk), or [Connect the AWS MCP Server](/operate/governance/remote-mcp-servers/aws) for fully worked examples. ## Where the server's tools become available diff --git a/app/en/operate/quickstart/page.mdx b/app/en/operate/quickstart/page.mdx index cc0136874..043c3d227 100644 --- a/app/en/operate/quickstart/page.mdx +++ b/app/en/operate/quickstart/page.mdx @@ -79,6 +79,8 @@ Tools can come from Arcade's pre-built catalog, an MCP server you deploy, or a [ Attach your User Source when you configure authentication for production. +To add the gateway in Claude for your organization, an Owner adds the gateway URL as a custom connector and keeps the **Detected** defaults. See [Keep the detected defaults](/get-started/mcp-clients/claude-desktop#keep-the-detected-defaults). + ### Turn on governance - **Contextual Access**: control tool visibility and behavior with hooks. Start with [Contextual Access](/operate/governance/contextual-access). diff --git a/app/en/references/auth-providers/salesforce/page.mdx b/app/en/references/auth-providers/salesforce/page.mdx index bcee88c99..6574b54dd 100644 --- a/app/en/references/auth-providers/salesforce/page.mdx +++ b/app/en/references/auth-providers/salesforce/page.mdx @@ -96,28 +96,32 @@ The Salesforce API requires the App developer to create [OAuth custom scopes](ht The custom scopes required by the [Arcade Salesforce MCP Server](/resources/integrations/sales/salesforce) are listed below, along with their descriptions: - - The custom scopes listed below are only required if you are using the [Arcade Salesforce MCP Server](/resources/integrations/sales/salesforce). - -If you're creating your own [custom Salesforce tools](/build/create-tools/tool-basics/build-mcp-server) or using Arcade to authorize users and call Salesforce APIs directly, you are free to define custom scope(s) that fit best your application use cases. Observe that you must have at least one custom scope assigned to your Salesforce app in order to use the Salesforce API. - - - - `read_account`: Read access to account data. - `read_contact`: Read access to contact data. - `read_lead`: Read access to lead data. - `read_note`: Read access to note data. - `read_opportunity`: Read access to opportunity data. - `read_task`: Read access to task data. +- `read_user`: Read access to user data. - `write_contact`: Write access to create contact. +- `write_lead`: Write access to create/update leads. +- `write_opportunity`: Write access to create/update opportunities. +- `write_task`: Write access to create/update tasks. Follow the [Create an OAuth Custom Scope](https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_customscopes_create.htm&type=5) and [Assign an OAuth Custom Scope to an External Client App](https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_customscopes_assign.htm&type=5) Salesforce documentation to understand how to define and assign these scopes to your Salesforce app. + + If you use the [Arcade Salesforce MCP Server](/resources/integrations/sales/salesforce), you must create, assign, and request every custom scope in the list above. Salesforce requires at least one custom scope on the app. One scope is not enough for the MCP server. + + + + The authorize URL will still include Salesforce's built-in `api` and `refresh_token` scopes from [Create a Salesforce app](#create-a-salesforce-app), even if you only pass custom names to `auth.start()`. Those are not custom scopes. Don't create them in the custom-scope UI. + + - The scope names aren't really attached to any endpoint or action. It's the - developer's job to honor the permissions communicated to the user when - authorizing the app. You could, in theory, assign one single scope (e.g. - `fullaccess`) and use it to query any Salesforce API endpoint. + If you're creating your own [custom Salesforce tools](/build/create-tools/tool-basics/build-mcp-server) or using Arcade to authorize users and call Salesforce APIs directly, you are free to define custom scopes that fit your application. You must assign at least one custom scope to your Salesforce app. + + For custom tools, the scope names aren't attached to any endpoint or action. Honor the permissions you communicate to the user when they authorize the app. You could assign one single scope (e.g. `fullaccess`) and use it to query any Salesforce API endpoint. ## Configuring Salesforce Auth @@ -149,19 +153,19 @@ Go to the [Arcade Dashboard](https://api.arcade.dev/dashboard) and log in with y #### Configure the auth endpoints - Replace `salesforce-org-subdomain` with your [Salesforce Org + Replace {``} with your [Salesforce Org Subdomain](#get-your-salesforce-org-subdomain). - Enter the auth endpoints: - - **Authorization Endpoint**: `https://salesforce-org-subdomain.my.salesforce.com/services/oauth2/authorize` - - **Token Endpoint**: `https://salesforce-org-subdomain.my.salesforce.com/services/oauth2/token` + - **Authorization Endpoint**: {`https://.my.salesforce.com/services/oauth2/authorize`} + - **Token Endpoint**: {`https://.my.salesforce.com/services/oauth2/token`} - Under **Refresh Token Settings**: - - Enter the **Refresh Token Endpoint**: `https://salesforce-org-subdomain.my.salesforce.com/services/oauth2/token` + - Enter the **Refresh Token Endpoint**: {`https://.my.salesforce.com/services/oauth2/token`} - In **Response Content Type**, select `application/json`. - Under **Token Introspection Settings**: - Check the **Enable Token Introspection** option. - - Enter the **Token Introspection Endpoint**: `https://salesforce-org-subdomain.my.salesforce.com/services/oauth2/introspect` + - Enter the **Token Introspection Endpoint**: {`https://.my.salesforce.com/services/oauth2/introspect`} - In **HTTP Method**, select `POST` - In **Authentication Method**, select `Client Secret Basic` - In **Request Content Type**, select `application/x-www-form-urlencoded`. @@ -243,7 +247,19 @@ client = Arcade(base_url="http://localhost:9099") # Automatically finds the `AR salesforce_provider_id = "salesforce" salesforce_org_subdomain = "salesforce-org-subdomain" user_id = "{arcade_user_id}" -scopes = ["read_account"] +scopes = [ + "read_account", + "read_contact", + "read_lead", + "read_note", + "read_opportunity", + "read_task", + "read_user", + "write_contact", + "write_lead", + "write_opportunity", + "write_task", +] ``` Here's a break down of each value: @@ -251,7 +267,7 @@ Here's a break down of each value: - **`salesforce_provider_id`**: the ID you entered when setting up the [Salesforce auth provider](#configuring-salesforce-auth); - **`salesforce_org_subdomain`**: your [Salesforce Org Subdomain](#get-your-salesforce-org-subdomain); - **`user_id`**: an internal identifier for your application user (it could be an email address, a username, UUID, etc); for demonstration purposes, in this example, enter your own email address; -- **`scopes`**: the list of scopes you want to request from the user; if you assigned the [custom scopes required by the Arcade Salesforce MCP Server](#create-and-assign-custom-scopes-to-your-external-client-app) use `["read_account"]` in this example. +- **`scopes`**: the custom scopes to request from the user. Creating and assigning them on the Salesforce app is not enough. `auth.start()` only consents to this list. For the [Arcade Salesforce MCP Server](#create-and-assign-custom-scopes-to-your-external-client-app), pass every scope in the list above. ### Start the authorization process and wait for completion @@ -329,7 +345,19 @@ client = Arcade(base_url="http://localhost:9099") # Automatically finds the `ARC salesforce_provider_id = "salesforce" salesforce_org_subdomain = "salesforce-org-subdomain" user_id = "{arcade_user_id}" -scopes = ["read_account"] +scopes = [ + "read_account", + "read_contact", + "read_lead", + "read_note", + "read_opportunity", + "read_task", + "read_user", + "write_contact", + "write_lead", + "write_opportunity", + "write_task", +] # Start the authorization process @@ -410,7 +438,19 @@ const client = new Arcade((baseURL = "http://localhost:9099")); // Automatically const salesforceProviderId = "salesforce"; const salesforceOrgSubdomain = "salesforce-org-subdomain"; const userId = "{arcade_user_id}"; -const scopes = ["read_account"]; +const scopes = [ + "read_account", + "read_contact", + "read_lead", + "read_note", + "read_opportunity", + "read_task", + "read_user", + "write_contact", + "write_lead", + "write_opportunity", + "write_task", +]; ``` Here's a break down of each value: @@ -418,7 +458,7 @@ Here's a break down of each value: - **`salesforceProviderId`**: the ID you entered when setting up the [Salesforce auth provider](#configuring-salesforce-auth); - **`salesforceOrgSubdomain`**: your [Salesforce Org Subdomain](#get-your-salesforce-org-subdomain); - **`userId`**: an internal identifier for your application user (it could be an email address, a username, UUID, etc); for demonstration purposes, in this example, enter your own email address; -- **`scopes`**: the list of scopes you want to request from the user; if you assigned the [custom scopes required by the Arcade Salesforce MCP Server](#create-and-assign-custom-scopes-to-your-external-client-app) use `["read_account"]` in this example. +- **`scopes`**: the custom scopes to request from the user. Creating and assigning them on the Salesforce app is not enough. `auth.start()` only consents to this list. For the [Arcade Salesforce MCP Server](#create-and-assign-custom-scopes-to-your-external-client-app), pass every scope in the list above. ### Start the authorization process and wait for completion @@ -505,7 +545,19 @@ const client = new Arcade((baseURL = "http://localhost:9099")); // Automatically const salesforceProviderId = "salesforce"; const salesforceOrgSubdomain = "salesforce-org-subdomain"; const userId = "{arcade_user_id}"; -const scopes = ["read_account"]; +const scopes = [ + "read_account", + "read_contact", + "read_lead", + "read_note", + "read_opportunity", + "read_task", + "read_user", + "write_contact", + "write_lead", + "write_opportunity", + "write_task", +]; // Start the authorization process let authResponse = await client.auth.start(userId, { diff --git a/app/layout.tsx b/app/layout.tsx index 12837dabe..bb9aab73f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,7 +6,6 @@ import { getDashboardUrl } from "@/app/_components/dashboard-link"; import { Footer } from "@/app/_components/footer"; import { Logo } from "@/app/_components/logo"; import NavBarButton from "@/app/_components/nav-bar-button"; -import { TranslationBanner } from "@/app/_components/translation-banner"; import "@/app/globals.css"; import { Discord, Github } from "@arcadeai/design-system"; import { GoogleTagManager } from "@next/third-parties/google"; @@ -96,15 +95,7 @@ export default async function RootLayout({ }: { children: React.ReactNode; }) { - // proxy.ts redirects every request to a "/en/..." path — "es" and - // "pt-BR" routes bounce to their "/en" equivalent and unlocaled routes - // pick up "/en" from getPreferredLocale, which is hardcoded to return - // "en" unconditionally. So this layout only ever renders under "/en", - // and reading the locale here can be a constant instead of a header - // lookup. Awaiting headers() in the root layout previously forced the - // entire route tree into dynamic rendering. Restoring real i18n means - // moving this layout under an `app/[lang]/` route segment so the - // locale comes from routing params, not a request header. + // Docs are English-only. proxy.ts redirects legacy /es and /pt-BR URLs to /en. const lang = "en"; const dictionary = await getDictionary(lang); @@ -149,9 +140,6 @@ export default async function RootLayout({ - {lang !== "en" && ( - - )} (null); const [dict, setDict] = useState(defaultDict); - const { currentLocale, englishPath, showEnglishLink } = useMemo(() => { - const locale = pathname.match(LOCALE_PATH_REGEX)?.[1] || "en"; - const enPath = `/en${pathname.replace(LOCALE_PREFIX_REGEX, "") || "/"}`; - const showEnLink = locale !== "en"; - - return { - currentLocale: locale, - englishPath: enPath, - showEnglishLink: showEnLink, - }; - }, [pathname]); + const currentLocale = useMemo( + () => pathname.match(LOCALE_PATH_REGEX)?.[1] || "en", + [pathname] + ); - // Load dictionary on client side useEffect(() => { const loadDict = async () => { try { - type DictModule = { default: Dictionary }; - let dictModule: DictModule; - if (currentLocale === "es") { - dictModule = await import("@/_dictionaries/es"); - } else if (currentLocale === "pt-BR") { - dictModule = await import("@/_dictionaries/pt-BR"); - } else { - dictModule = await import("@/_dictionaries/en"); - } + const dictModule = await import("@/_dictionaries/en"); setDict(dictModule.default.notFoundPage); } catch { // Keep default English dictionary on error } }; loadDict(); - }, [currentLocale]); + }, []); useEffect(() => { const search = searchParams?.toString(); @@ -105,14 +87,12 @@ function NotFoundContent() { pathname, path: pathWithQuery, locale: currentLocale, - english_path: englishPath, - show_english_link: showEnglishLink, // Ensure special props are attached to custom 404 events for link analysis. $current_url: typeof window !== "undefined" ? window.location.href : null, $referrer: referrer || null, $referring_domain: referringDomain || null, }); - }, [currentLocale, englishPath, pathname, searchParams, showEnglishLink]); + }, [currentLocale, pathname, searchParams]); return (
@@ -136,20 +116,6 @@ function NotFoundContent() {

- {/* English fallback */} - {showEnglishLink && ( -
- {dict.translationHint} - - {dict.viewOriginalEnglish} - - . -
- )} - {/* Actions */}