diff --git a/.github/workflows/check-redirects.yml b/.github/workflows/check-redirects.yml new file mode 100644 index 000000000..9b76d4aef --- /dev/null +++ b/.github/workflows/check-redirects.yml @@ -0,0 +1,127 @@ +name: Check Redirects for Deleted Pages + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - "app/**/*.md" + - "app/**/*.mdx" + - "next.config.ts" + +permissions: + contents: read + pull-requests: write + +jobs: + check-redirects: + name: Verify Deleted Pages Have Redirects + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history needed for branch comparison + + - name: Fetch base branch + run: git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} + + - name: Check for missing redirects + id: check + run: | + set -o pipefail + chmod +x ./scripts/check-redirects.sh + ./scripts/check-redirects.sh ${{ github.base_ref }} 2>&1 | tee redirect-check-output.txt + continue-on-error: true + + - name: Comment on PR if redirects are missing + if: steps.check.outcome == 'failure' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('redirect-check-output.txt', 'utf8'); + + // Extract the missing redirects and suggestions from output + const body = `## šŸ”— Missing Redirects Detected + + This PR deletes markdown files that don't have corresponding redirects in \`next.config.ts\`. + + When you delete a page, you must add a redirect to prevent broken links for users who have bookmarked the old URL. + +
+ šŸ“‹ View Details + + \`\`\` + ${output} + \`\`\` + +
+ + ### How to fix + + 1. Open \`next.config.ts\` + 2. Find the \`redirects()\` function + 3. Add redirect entries for each deleted file (see suggestions above) + 4. Push the changes + + --- + *This check ensures we maintain URL stability for our documentation.*`; + + // Check if we already commented + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Missing Redirects Detected') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + - name: Remove outdated comment if check passes + if: steps.check.outcome == 'success' + uses: actions/github-script@v7 + with: + script: | + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Missing Redirects Detected') + ); + + if (botComment) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + }); + } + + - name: Fail if redirects are missing + if: steps.check.outcome == 'failure' + run: | + echo "āŒ Missing redirects for deleted pages. See PR comment for details." + exit 1 diff --git a/.husky/pre-commit b/.husky/pre-commit index 8a2bd82e0..84723b893 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -29,6 +29,50 @@ if [ -n "$STAGED_DOCS" ]; then fi fi +# --- Check Redirects (when markdown pages are deleted) --- +# If any page.md/page.mdx files are being deleted, auto-add redirect entries +DELETED_PAGES=$(git diff --cached --name-status | grep -E "^D.*page\.(md|mdx)$" | cut -f2 || true) + +if [ -n "$DELETED_PAGES" ]; then + echo "šŸ”— Detected deleted page(s), checking for redirects..." + + # Run the redirect checker with auto-fix (compares staged changes to HEAD) + # This will add redirect entries to next.config.ts if missing + if ! ./scripts/check-redirects.sh --auto-fix HEAD 2>&1; then + # Stage next.config.ts if it was modified + if git diff --name-only next.config.ts 2>/dev/null | grep -q "next.config.ts"; then + git add next.config.ts + echo "" + echo "šŸ“ Redirect entries added to next.config.ts and staged." + fi + echo "" + # Check if there are placeholders vs other errors + if grep -q "REPLACE_WITH_NEW_PATH" next.config.ts 2>/dev/null; then + echo "āŒ Commit blocked: Please update the placeholder destinations in next.config.ts" + echo " Search for 'REPLACE_WITH_NEW_PATH' and provide actual redirect paths." + else + echo "āŒ Commit blocked: Please fix the redirect issues shown above." + fi + exit 1 + fi +fi + +# --- Update Internal Links (when redirects are added) --- +# If next.config.ts is staged, update any internal links pointing to redirected paths +if git diff --cached --name-only | grep -q "next.config.ts"; then + echo "šŸ”— Updating internal links for new redirects..." + + # Run the update script + if ./scripts/update-internal-links.sh 2>/dev/null; then + # Stage any files that were modified + UPDATED_FILES=$(git diff --name-only -- 'app/**/*.mdx' 'app/**/*.tsx' 'app/**/*.md' 2>/dev/null || true) + if [ -n "$UPDATED_FILES" ]; then + echo "$UPDATED_FILES" | xargs git add + echo "āœ… Internal links updated and staged" + fi + fi +fi + # --- Lint Staged (formatting) --- pnpm dlx lint-staged diff --git a/app/_components/tool-footer.tsx b/app/_components/tool-footer.tsx index 749498a0e..bbac5ae9f 100644 --- a/app/_components/tool-footer.tsx +++ b/app/_components/tool-footer.tsx @@ -15,7 +15,7 @@ const ToolFooter: React.FC = ({ pipPackageName }) => (
@@ -24,7 +24,7 @@ const ToolFooter: React.FC = ({ pipPackageName }) => ( description={ "Arcade tools can be self-hosted on your own infrastructure. Learn more about self-hosting." } - href="/home/hosting-overview" + href="/guides/deployment-hosting" icon={Puzzle} title="Self Host Arcade tools" /> diff --git a/app/en/guides/agent-frameworks/_meta.tsx b/app/en/get-started/agent-frameworks/_meta.tsx similarity index 80% rename from app/en/guides/agent-frameworks/_meta.tsx rename to app/en/get-started/agent-frameworks/_meta.tsx index 294221288..3ebbaae3a 100644 --- a/app/en/guides/agent-frameworks/_meta.tsx +++ b/app/en/get-started/agent-frameworks/_meta.tsx @@ -4,6 +4,9 @@ export const meta: MetaRecord = { index: { title: "Overview", }, + "setup-arcade-with-your-llm-python": { + title: "Setup Arcade with your LLM (Python)", + }, crewai: { title: "CrewAI", }, diff --git a/app/en/guides/agent-frameworks/crewai/_meta.tsx b/app/en/get-started/agent-frameworks/crewai/_meta.tsx similarity index 100% rename from app/en/guides/agent-frameworks/crewai/_meta.tsx rename to app/en/get-started/agent-frameworks/crewai/_meta.tsx diff --git a/app/en/guides/agent-frameworks/crewai/custom-auth-flow/page.mdx b/app/en/get-started/agent-frameworks/crewai/custom-auth-flow/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/crewai/custom-auth-flow/page.mdx rename to app/en/get-started/agent-frameworks/crewai/custom-auth-flow/page.mdx diff --git a/app/en/guides/agent-frameworks/crewai/use-arcade-tools/page.mdx b/app/en/get-started/agent-frameworks/crewai/use-arcade-tools/page.mdx similarity index 98% rename from app/en/guides/agent-frameworks/crewai/use-arcade-tools/page.mdx rename to app/en/get-started/agent-frameworks/crewai/use-arcade-tools/page.mdx index 3b975f314..127e8e095 100644 --- a/app/en/guides/agent-frameworks/crewai/use-arcade-tools/page.mdx +++ b/app/en/get-started/agent-frameworks/crewai/use-arcade-tools/page.mdx @@ -10,7 +10,7 @@ import ToggleContent from "@/app/_components/toggle-content"; In this guide, we will explore how to integrate Arcade tools into your CrewAI application. Follow the step-by-step instructions below. If a tool requires authorization, an authorization URL will appear in the console, waiting for your approval. This process ensures that only the tools you choose to authorize are executed. -To tailor the tool authorization flow to meet your application's specific needs, check out the [Custom Auth Flow with CrewAI](/guides/agent-frameworks/crewai/custom-auth-flow) guide. +To tailor the tool authorization flow to meet your application's specific needs, check out the [Custom Auth Flow with CrewAI](/get-started/agent-frameworks/crewai/custom-auth-flow) guide. diff --git a/app/en/guides/agent-frameworks/google-adk/_meta.tsx b/app/en/get-started/agent-frameworks/google-adk/_meta.tsx similarity index 100% rename from app/en/guides/agent-frameworks/google-adk/_meta.tsx rename to app/en/get-started/agent-frameworks/google-adk/_meta.tsx diff --git a/app/en/guides/agent-frameworks/google-adk/overview/page.mdx b/app/en/get-started/agent-frameworks/google-adk/overview/page.mdx similarity index 96% rename from app/en/guides/agent-frameworks/google-adk/overview/page.mdx rename to app/en/get-started/agent-frameworks/google-adk/overview/page.mdx index 6f97f8457..be1bd59d2 100644 --- a/app/en/guides/agent-frameworks/google-adk/overview/page.mdx +++ b/app/en/get-started/agent-frameworks/google-adk/overview/page.mdx @@ -127,7 +127,7 @@ for tool in google_tools: Ready to start building with Arcade and OpenAI Agents? Check out these guides: -- [Using Arcade tools](/guides/agent-frameworks/google-adk/use-arcade-tools) - Learn the basics of using Arcade tools with Google ADK +- [Using Arcade tools](/get-started/agent-frameworks/google-adk/use-arcade-tools) - Learn the basics of using Arcade tools with Google ADK - [Creating custom tools](/guides/create-tools/tool-basics/build-mcp-server) - Build your own tools with the Arcade Tool SDK Enjoy exploring Arcade and building powerful AI-enabled applications! diff --git a/app/en/guides/agent-frameworks/google-adk/use-arcade-tools/page.mdx b/app/en/get-started/agent-frameworks/google-adk/use-arcade-tools/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/google-adk/use-arcade-tools/page.mdx rename to app/en/get-started/agent-frameworks/google-adk/use-arcade-tools/page.mdx diff --git a/app/en/guides/agent-frameworks/langchain/_meta.tsx b/app/en/get-started/agent-frameworks/langchain/_meta.tsx similarity index 100% rename from app/en/guides/agent-frameworks/langchain/_meta.tsx rename to app/en/get-started/agent-frameworks/langchain/_meta.tsx diff --git a/app/en/guides/agent-frameworks/langchain/auth-langchain-tools/page.mdx b/app/en/get-started/agent-frameworks/langchain/auth-langchain-tools/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/langchain/auth-langchain-tools/page.mdx rename to app/en/get-started/agent-frameworks/langchain/auth-langchain-tools/page.mdx diff --git a/app/en/guides/agent-frameworks/langchain/use-arcade-tools/page.mdx b/app/en/get-started/agent-frameworks/langchain/use-arcade-tools/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/langchain/use-arcade-tools/page.mdx rename to app/en/get-started/agent-frameworks/langchain/use-arcade-tools/page.mdx diff --git a/app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx b/app/en/get-started/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx rename to app/en/get-started/agent-frameworks/langchain/use-arcade-with-langchain/page.mdx diff --git a/app/en/guides/agent-frameworks/langchain/user-auth-interrupts/page.mdx b/app/en/get-started/agent-frameworks/langchain/user-auth-interrupts/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/langchain/user-auth-interrupts/page.mdx rename to app/en/get-started/agent-frameworks/langchain/user-auth-interrupts/page.mdx diff --git a/app/en/guides/agent-frameworks/mastra/_meta.tsx b/app/en/get-started/agent-frameworks/mastra/_meta.tsx similarity index 100% rename from app/en/guides/agent-frameworks/mastra/_meta.tsx rename to app/en/get-started/agent-frameworks/mastra/_meta.tsx diff --git a/app/en/guides/agent-frameworks/mastra/overview/page.mdx b/app/en/get-started/agent-frameworks/mastra/overview/page.mdx similarity index 87% rename from app/en/guides/agent-frameworks/mastra/overview/page.mdx rename to app/en/get-started/agent-frameworks/mastra/overview/page.mdx index 12ab8b334..53dd45c2a 100644 --- a/app/en/guides/agent-frameworks/mastra/overview/page.mdx +++ b/app/en/get-started/agent-frameworks/mastra/overview/page.mdx @@ -29,5 +29,5 @@ The integration works through three key mechanisms: ### Next Steps -- Learn how to [use Arcade tools](/guides/agent-frameworks/mastra/use-arcade-tools) in a Mastra agent -- Implement [user authentication handling](/guides/agent-frameworks/mastra/user-auth-interrupts) for tools in multi-user applications +- Learn how to [use Arcade tools](/get-started/agent-frameworks/mastra/use-arcade-tools) in a Mastra agent +- Implement [user authentication handling](/get-started/agent-frameworks/mastra/user-auth-interrupts) for tools in multi-user applications diff --git a/app/en/guides/agent-frameworks/mastra/use-arcade-tools/page.mdx b/app/en/get-started/agent-frameworks/mastra/use-arcade-tools/page.mdx similarity index 96% rename from app/en/guides/agent-frameworks/mastra/use-arcade-tools/page.mdx rename to app/en/get-started/agent-frameworks/mastra/use-arcade-tools/page.mdx index da44a2469..9ab8f2d1b 100644 --- a/app/en/guides/agent-frameworks/mastra/use-arcade-tools/page.mdx +++ b/app/en/get-started/agent-frameworks/mastra/use-arcade-tools/page.mdx @@ -158,4 +158,4 @@ for await (const chunk of stream.textStream) { -When running your agent for the first time with tools that require user consent (like Google or Github), the agent will return an authorization reponse (e.g., `{ authorization_required: true, url: '...', message: '...' }`). Your agent's instructions should guide it to present this URL to the user. After the user visits this URL and grants permissions, the tool can be used successfully. See the [Managing user authorization](/guides/agent-frameworks/mastra/user-auth-interrupts) guide for more details on handling authentication flows. +When running your agent for the first time with tools that require user consent (like Google or Github), the agent will return an authorization reponse (e.g., `{ authorization_required: true, url: '...', message: '...' }`). Your agent's instructions should guide it to present this URL to the user. After the user visits this URL and grants permissions, the tool can be used successfully. See the [Managing user authorization](/get-started/agent-frameworks/mastra/user-auth-interrupts) guide for more details on handling authentication flows. diff --git a/app/en/guides/agent-frameworks/mastra/user-auth-interrupts/page.mdx b/app/en/get-started/agent-frameworks/mastra/user-auth-interrupts/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/mastra/user-auth-interrupts/page.mdx rename to app/en/get-started/agent-frameworks/mastra/user-auth-interrupts/page.mdx diff --git a/app/en/guides/agent-frameworks/openai-agents/_meta.tsx b/app/en/get-started/agent-frameworks/openai-agents/_meta.tsx similarity index 100% rename from app/en/guides/agent-frameworks/openai-agents/_meta.tsx rename to app/en/get-started/agent-frameworks/openai-agents/_meta.tsx diff --git a/app/en/guides/agent-frameworks/openai-agents/overview/page.mdx b/app/en/get-started/agent-frameworks/openai-agents/overview/page.mdx similarity index 95% rename from app/en/guides/agent-frameworks/openai-agents/overview/page.mdx rename to app/en/get-started/agent-frameworks/openai-agents/overview/page.mdx index 73e6b5384..c992e9209 100644 --- a/app/en/guides/agent-frameworks/openai-agents/overview/page.mdx +++ b/app/en/get-started/agent-frameworks/openai-agents/overview/page.mdx @@ -177,8 +177,8 @@ For a full list of available MCP Servers, visit the [Arcade MCP Servers](/resour Ready to start building with Arcade and OpenAI Agents? Check out these guides: -- [Using Arcade tools](/guides/agent-frameworks/openai-agents/use-arcade-tools) - Learn the basics of using Arcade tools with OpenAI Agents -- [Managing user authorization](/guides/agent-frameworks/openai-agents/user-auth-interrupts) - Handle tool authorization efficiently +- [Using Arcade tools](/get-started/agent-frameworks/openai-agents/use-arcade-tools) - Learn the basics of using Arcade tools with OpenAI Agents +- [Managing user authorization](/get-started/agent-frameworks/openai-agents/user-auth-interrupts) - Handle tool authorization efficiently - [Creating custom tools](/guides/create-tools/tool-basics/build-mcp-server) - Build your own tools with the Arcade Tool SDK Enjoy exploring Arcade and building powerful AI-enabled applications! diff --git a/app/en/guides/agent-frameworks/openai-agents/use-arcade-tools/page.mdx b/app/en/get-started/agent-frameworks/openai-agents/use-arcade-tools/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/openai-agents/use-arcade-tools/page.mdx rename to app/en/get-started/agent-frameworks/openai-agents/use-arcade-tools/page.mdx diff --git a/app/en/guides/agent-frameworks/openai-agents/user-auth-interrupts/page.mdx b/app/en/get-started/agent-frameworks/openai-agents/user-auth-interrupts/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/openai-agents/user-auth-interrupts/page.mdx rename to app/en/get-started/agent-frameworks/openai-agents/user-auth-interrupts/page.mdx diff --git a/app/en/guides/agent-frameworks/page.mdx b/app/en/get-started/agent-frameworks/page.mdx similarity index 99% rename from app/en/guides/agent-frameworks/page.mdx rename to app/en/get-started/agent-frameworks/page.mdx index 12716a8a2..879262657 100644 --- a/app/en/guides/agent-frameworks/page.mdx +++ b/app/en/get-started/agent-frameworks/page.mdx @@ -1,4 +1,3 @@ -```mdx import { PlatformCard } from "@/app/_components/platform-card"; import { Tabs } from "nextra/components"; @@ -72,4 +71,3 @@ These guides are for developers building AI applications who need to connect Arc
-``` diff --git a/app/en/get-started/agent-frameworks/setup-arcade-with-your-llm-python/page.mdx b/app/en/get-started/agent-frameworks/setup-arcade-with-your-llm-python/page.mdx new file mode 100644 index 000000000..9e652555b --- /dev/null +++ b/app/en/get-started/agent-frameworks/setup-arcade-with-your-llm-python/page.mdx @@ -0,0 +1,499 @@ +--- +title: "Connect Arcade to LLM (Python)" +description: "Learn how to connect Arcade to your LLM in Python" +--- + +import { Steps, Tabs, Callout } from "nextra/components"; +import { SignupLink } from "@/app/_components/analytics"; + +# Connect Arcade to your LLM + +Arcade tools work alongside an LLM. To make that work, you need a small piece of glue code called a "harness." The harness orchestrates the back-and-forth between the user, the model, and the tools. In this guide, you'll build one so you can wire Arcade into your LLM-powered app. + + + + +Integrate Arcade's tool-calling capabilities into an application that uses an LLM in Python. + + + + + +- An Arcade account +- An [Arcade API key](/get-started/setup/api-keys) +- An [OpenRouter API key](https://openrouter.ai/docs/guides/overview/auth/provisioning-api-keys) +- The [`uv` package manager](https://docs.astral.sh/uv/getting-started/installation/) + + + + + +- Setup an agentic loop +- Add Arcade tools to your agentic loop +- Implement a multi-turn conversation loop + + + + + + +### Create a new project and install the dependencies + +In your terminal, run the following command to create a new `uv` project + +```bash +mkdir arcade-llm-example +cd arcade-llm-example +uv init +``` + +Create a new virtual environment and activate it: + +```bash +uv venv +source .venv/bin/activate +``` + +Install the dependencies: + +```bash +uv add arcadepy openai python-dotenv +``` + +Your directory should now look like this: + +```bash +arcade-llm-example/ +ā”œā”€ā”€ .git/ +ā”œā”€ā”€ .gitignore +ā”œā”€ā”€ python-version +ā”œā”€ā”€ .venv/ +ā”œā”€ā”€ main.py +ā”œā”€ā”€ pyproject.toml +ā”œā”€ā”€ main.py +ā”œā”€ā”€ README.md +└── uv.lock +``` + +### Instantiate and use the clients + +Create a new file called `.env` and add your Arcade API key, as well as your OpenAI API key: + +```text filename=".env" +ARCADE_API_KEY=YOUR_ARCADE_API_KEY +ARCADE_USER_ID=YOUR_ARCADE_USER_ID +OPENROUTER_API_KEY=YOUR_OPENROUTER_API_KEY +OPENROUTER_MODEL=YOUR_OPENROUTER_MODEL +``` + + + The `ARCADE_USER_ID` is the email address you used to sign up for Arcade. When + your app is ready for production, you can set this dynamically based on your + app's auth system. Learn more about how to achieve secure auth in production + [here](/guides/user-facing-agents/secure-auth-production). + + + + In this example, you're using OpenRouter to access the model, as it makes it + straightforward to use any model from multiple providers with a single API. + +OpenRouter is compliant with the OpenAI API specification, so you can use it +with any OpenAI-compatible library. + +If you don't know which model to use, try one of these: + +- `anthropic/claude-haiku-4.5` +- `deepseek/deepseek-v3.2` +- `google/gemini-3-flash-preview` +- `google/gemini-2.5-flash-lite` +- `openai/gpt-4o-mini` + + + +Open the `main.py` file in your editor of choice, and replace the contents with the following: + +```python filename="main.py" +from arcadepy import Arcade +from openai import OpenAI +from dotenv import load_dotenv +import json +import os + +load_dotenv() + +arcade_client = Arcade() +arcade_user_id = os.getenv("ARCADE_USER_ID") +llm_client = OpenAI( + api_key=os.getenv("OPENROUTER_API_KEY"), + base_url="https://openrouter.ai/api/v1" +) + +``` + +### Select and retrieve the tools from Arcade + +In this example, you're implementing a multi-tool agent that can retrieve and send emails, as well as send messages to Slack. While a harness can expose a broad catalog of tools to the LLM, it's best to limit that set to what's relevant for the task to keep the model efficient. + +```python filename="main.py" +# Define the tools for the agent to use +tool_catalog = [ + "Gmail.ListEmails", + "Gmail.SendEmail", + "Slack.SendMessage", + "Slack.WhoAmI" +] + +# Get the tool definitions from the Arcade API +tool_definitions = [] +for tool in tool_catalog: + tool_definitions.append(arcade_client.tools.formatted.get(name=tool, format="openai")) +``` + +### Write a helper function that handles tool authorization and execution + +The model can use any tool you give it, and some tools require permission before they work. When this happens, you can either involve the model in the permission step or handle it behind the scenes and continue as if the tool were already authorized. In this guide, authorization happens outside the model so it can act as if the tool is already available. It's like ordering a coffee: after you place your order, the barista handles payment behind the counter instead of explaining every step of card verification and receipts. The customer (and the model) gets the result without having to think about any of the intermediate steps. + +```python filename="main.py" +# Helper function to authorize and run any tool +def authorize_and_run_tool(tool_name: str, input: str): + # Start the authorization process + auth_response = arcade_client.tools.authorize( + tool_name=tool_name, + user_id=arcade_user_id, + ) + + # If the authorization is not completed, print the authorization URL and wait for the user to authorize the app. + # Tools that do not require authorization will have the status "completed" already. + if auth_response.status != "completed": + print(f"Click this link to authorize {tool_name}: {auth_response.url}. The process will continue once you have authorized the app.") + arcade_client.auth.wait_for_completion(auth_response.id) + + # Parse the input + input_json = json.loads(input) + + # Run the tool + result = arcade_client.tools.execute( + tool_name=tool_name, + input=input_json, + user_id=arcade_user_id, + ) + + # Return the tool output to the caller as a JSON string + return json.dumps(result.output.value) +``` + +This helper function adapts to any tool in the catalog and will make sure that the authorization requirements are met before executing the tool. For more complex agentic patterns, this is generally the best place to handle interruptions that may require user interaction, such as when the tool requires a user to approve a request, or to provide additional context. + +### Write a helper function that handles the LLM's invocation + +There are many orchestration patterns that can be used to handle the LLM invocation. A common pattern is a ReAct architecture, where the user prompt will result in a loop of messages between the LLM and the tools, until the LLM provides a final response (no tool calls). This is the pattern we will implement in this example. + +To avoid the risk of infinite loops, limit the number of turns (in this case, a maximum of 5). This is a parameter that you can tune to your needs. Set it to a value that is high enough to allow the LLM to complete its task but low enough to prevent infinite loops. + +```python filename="main.py" +def invoke_llm( + history: list[dict], + model: str = "google/gemini-2.5-flash", + max_turns: int = 5, + tools: list[dict] = None, + tool_choice: str = "auto", +) -> list[dict]: + """ + Multi-turn LLM invocation that processes the conversation until + the assistant provides a final response (no tool calls). + + Returns the updated conversation history. + """ + turns = 0 + + while turns < max_turns: + turns += 1 + + response = llm_client.chat.completions.create( + model=model, + messages=history, + tools=tools, + tool_choice=tool_choice, + ) + + assistant_message = response.choices[0].message + + if assistant_message.tool_calls: + # Append the assistant message with tool_calls to history first + history.append({ + "role": "assistant", + "content": assistant_message.content, + "tool_calls": [ + { + "id": tc.id, + "type": "function", + "function": {"name": tc.function.name, "arguments": tc.function.arguments} + } + for tc in assistant_message.tool_calls + ] + }) + + for tool_call in assistant_message.tool_calls: + tool_name = tool_call.function.name + tool_args = tool_call.function.arguments + print(f"šŸ› ļø Harness: Calling {tool_name} with input {tool_args}") + tool_result = authorize_and_run_tool(tool_name, tool_args) + print(f"šŸ› ļø Harness: Tool call {tool_name} completed") + history.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": tool_result, + }) + + continue + + else: + history.append({ + "role": "assistant", + "content": assistant_message.content, + }) + + break + + return history +``` + +These two helper functions form the core of your agentic loop. Notice that authorization is handled outside the agentic context, and the tool execution is passed back to the LLM in every case. Depending on your needs, you may want to handle tool orchestration within the harness and pass only the final result of multiple tool calls to the LLM. + +### Write the main agentic loop + +Now that you've written the helper functions, write an agentic loop that interacts with the user. The core pieces of this loop are: + +1. Initialize the conversation history with the system prompt +2. Get the user input and add it to the conversation history +3. Invoke the LLM with the conversation history, tools, and tool choice +4. Repeat from step 2 until the user decides to stop the conversation + +```python filename="main.py" +def chat(): + """Interactive multi-turn chat session.""" + print("Chat started. Type 'quit' or 'exit' to end the session.\n") + + # Initialize the conversation history with the system prompt + history: list[dict] = [ + {"role": "system", "content": "You are a helpful assistant."} + ] + + while True: + try: + user_input = input("šŸ˜Ž You: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nGoodbye!") + break + + if not user_input: + continue + + if user_input.lower() in ("quit", "exit"): + print("Goodbye!") + break + + # Add user message to history + history.append({"role": "user", "content": user_input}) + + # Get LLM response + history = invoke_llm( + history, tools=tool_definitions) + + # Print the latest assistant response + assistant_response = history[-1]["content"] + print(f"\nšŸ¤– Assistant: {assistant_response}\n") + + +if __name__ == "__main__": + chat() +``` + +### Run the code + +It's time to run the code and see it in action. Run the following command to start the chat: + +```bash +uv run main.py +``` + +With the selection of tools above, you should be able to get the agent to effectively complete the following prompts: + +- "Please send a message to the #general channel on Slack greeting everyone with a haiku about agents." +- "Please write a poem about multi-tool orchestration and send it to the #general channel on Slack, also send it to me in an email." +- "Please summarize my latest 5 emails, then send me a DM on Slack with the summary." + + + +## Next Steps + +- Learn more about using Arcade with a [framework](/get-started/agent-frameworks) or [MCP client](/get-started/mcp-clients). +- Learn more about how to [build your own MCP Servers](/guides/create-tools/tool-basics/build-mcp-server). + +## Example code + +
+**main.py** (full file) +```python filename="main.py" +from arcadepy import Arcade +from dotenv import load_dotenv +from openai import OpenAI +import json +import os + +load_dotenv() + +arcade_client = Arcade() +arcade_user_id = os.getenv("ARCADE_USER_ID") +llm_client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=os.getenv("OPENROUTER_API_KEY"), +) + +# Define the tools to use in the agent +tool_catalog = [ + "Gmail.ListEmails", + "Gmail.SendEmail", + "Slack.SendMessage", + "Slack.WhoAmI" +] + +# Get the tool definitions from the Arcade API to expose them to the LLM +tool_definitions = [] +for tool in tool_catalog: + tool_definitions.append(arcade_client.tools.formatted.get(name=tool, format="openai")) + + +# Helper function to authorize and run any tool +def authorize_and_run_tool(tool_name: str, input: str): + # Start the authorization process + auth_response = arcade_client.tools.authorize( + tool_name=tool_name, + user_id=arcade_user_id, + ) + + # If the authorization is not completed, print the authorization URL and wait for the user to authorize the app. + # Tools that do not require authorization will have the status "completed" already. + if auth_response.status != "completed": + print(f"Click this link to authorize {tool_name}: {auth_response.url}. The process will continue once you have authorized the app.") + arcade_client.auth.wait_for_completion(auth_response.id) + + # Parse the input + input_json = json.loads(input) + + # Run the tool + result = arcade_client.tools.execute( + tool_name=tool_name, + input=input_json, + user_id=arcade_user_id, + ) + + # Return the tool output to the caller as a JSON string + return json.dumps(result.output.value) + + +def invoke_llm( + history: list[dict], + model: str = "google/gemini-2.5-flash", + max_turns: int = 5, + tools: list[dict] = None, + tool_choice: str = "auto", +) -> list[dict]: + """ + Multi-turn LLM invocation that processes the conversation until + the assistant provides a final response (no tool calls). + + Returns the updated conversation history. + """ + turns = 0 + + while turns < max_turns: + turns += 1 + + response = llm_client.chat.completions.create( + model=model, + messages=history, + tools=tools, + tool_choice=tool_choice, + ) + + assistant_message = response.choices[0].message + + if assistant_message.tool_calls: + # Append the assistant message with tool_calls to history first + history.append({ + "role": "assistant", + "content": assistant_message.content, + "tool_calls": [ + { + "id": tc.id, + "type": "function", + "function": {"name": tc.function.name, "arguments": tc.function.arguments} + } + for tc in assistant_message.tool_calls + ] + }) + + for tool_call in assistant_message.tool_calls: + tool_name = tool_call.function.name + tool_args = tool_call.function.arguments + print(f"šŸ› ļø Harness: Calling {tool_name} with input {tool_args}") + tool_result = authorize_and_run_tool(tool_name, tool_args) + print(f"šŸ› ļø Harness: Tool call {tool_name} completed") + history.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": tool_result, + }) + + continue + + else: + history.append({ + "role": "assistant", + "content": assistant_message.content, + }) + + break + + return history + + +def chat(): + """Interactive multi-turn chat session.""" + print("Chat started. Type 'quit' or 'exit' to end the session.\n") + + history: list[dict] = [ + {"role": "system", "content": "You are a helpful assistant."} + ] + + while True: + try: + user_input = input("šŸ˜Ž You: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nGoodbye!") + break + + if not user_input: + continue + + if user_input.lower() in ("quit", "exit"): + print("Goodbye!") + break + + # Add user message to history + history.append({"role": "user", "content": user_input}) + + # Get LLM response + history = invoke_llm( + history, tools=tool_definitions) + + # Print the latest assistant response + assistant_response = history[-1]["content"] + print(f"\nšŸ¤– Assistant: {assistant_response}\n") + + +if __name__ == "__main__": + chat() +``` +
diff --git a/app/en/guides/agent-frameworks/vercelai/page.mdx b/app/en/get-started/agent-frameworks/vercelai/page.mdx similarity index 100% rename from app/en/guides/agent-frameworks/vercelai/page.mdx rename to app/en/get-started/agent-frameworks/vercelai/page.mdx diff --git a/app/en/guides/tool-calling/mcp-clients/_meta.tsx b/app/en/get-started/mcp-clients/_meta.tsx similarity index 85% rename from app/en/guides/tool-calling/mcp-clients/_meta.tsx rename to app/en/get-started/mcp-clients/_meta.tsx index cb9deae1a..c54cd6ef1 100644 --- a/app/en/guides/tool-calling/mcp-clients/_meta.tsx +++ b/app/en/get-started/mcp-clients/_meta.tsx @@ -20,6 +20,9 @@ const meta: MetaRecord = { "visual-studio-code": { title: "Visual Studio Code", }, + "copilot-studio": { + title: "Microsoft Copilot Studio", + }, }; export default meta; diff --git a/app/en/guides/tool-calling/mcp-clients/claude-desktop/page.mdx b/app/en/get-started/mcp-clients/claude-desktop/page.mdx similarity index 100% rename from app/en/guides/tool-calling/mcp-clients/claude-desktop/page.mdx rename to app/en/get-started/mcp-clients/claude-desktop/page.mdx diff --git a/app/en/guides/tool-calling/mcp-clients/cursor/page.mdx b/app/en/get-started/mcp-clients/cursor/page.mdx similarity index 100% rename from app/en/guides/tool-calling/mcp-clients/cursor/page.mdx rename to app/en/get-started/mcp-clients/cursor/page.mdx diff --git a/app/en/guides/tool-calling/mcp-clients/mcp-client-grid.tsx b/app/en/get-started/mcp-clients/mcp-client-grid.tsx similarity index 93% rename from app/en/guides/tool-calling/mcp-clients/mcp-client-grid.tsx rename to app/en/get-started/mcp-clients/mcp-client-grid.tsx index 065586959..9dc79abc8 100644 --- a/app/en/guides/tool-calling/mcp-clients/mcp-client-grid.tsx +++ b/app/en/get-started/mcp-clients/mcp-client-grid.tsx @@ -21,6 +21,11 @@ const mcpClients = [ name: "Visual Studio Code", description: "Microsoft's code editor with MCP extensions", }, + { + key: "copilot-studio", + name: "Microsoft Copilot Studio", + description: "Microsoft's AI agent platform with MCP integration", + }, ]; // Logo mapping @@ -28,6 +33,7 @@ const logoSrc = { cursor: "/images/icons/cursor.png", "claude-desktop": "/images/icons/claude.png", "visual-studio-code": "/images/icons/vscode.svg", + "copilot-studio": "/images/icons/microsoft-copilot-studio.png", }; export function MCPClientGrid() { diff --git a/app/en/guides/tool-calling/mcp-clients/page.mdx b/app/en/get-started/mcp-clients/page.mdx similarity index 100% rename from app/en/guides/tool-calling/mcp-clients/page.mdx rename to app/en/get-started/mcp-clients/page.mdx diff --git a/app/en/guides/tool-calling/mcp-clients/visual-studio-code/page.mdx b/app/en/get-started/mcp-clients/visual-studio-code/page.mdx similarity index 100% rename from app/en/guides/tool-calling/mcp-clients/visual-studio-code/page.mdx rename to app/en/get-started/mcp-clients/visual-studio-code/page.mdx diff --git a/app/en/get-started/quickstarts/call-tool-agent/page.mdx b/app/en/get-started/quickstarts/call-tool-agent/page.mdx index a4cbbc17c..4c2cd73c8 100644 --- a/app/en/get-started/quickstarts/call-tool-agent/page.mdx +++ b/app/en/get-started/quickstarts/call-tool-agent/page.mdx @@ -356,7 +356,7 @@ console.log(respose_send_email.output?.value); ## Next Steps -In this simple example, we call the tool methods directly. In your real applications and agents, you'll likely be letting the LLM decide which tools to call. Learn more about using Arcade with Frameworks in the [Frameworks](/guides/agent-frameworks) section, or [how to build your own tools](/guides/create-tools/tool-basics/build-mcp-server). +In this simple example, we call the tool methods directly. In your real applications and agents, you'll likely be letting the LLM decide which tools to call. Learn more about using Arcade with Frameworks in the [Frameworks](/get-started/agent-frameworks) section, or [how to build your own tools](/guides/create-tools/tool-basics/build-mcp-server). ## Example Code diff --git a/app/en/get-started/quickstarts/call-tool-client/page.mdx b/app/en/get-started/quickstarts/call-tool-client/page.mdx index 449ebea24..4821d355a 100644 --- a/app/en/get-started/quickstarts/call-tool-client/page.mdx +++ b/app/en/get-started/quickstarts/call-tool-client/page.mdx @@ -176,6 +176,6 @@ As you interact with the agent, it will call the tools from the MCP Gateway. You - Learn more about [MCP Gateways](/guides/create-tools/mcp-gateways). - Learn how to use MCP Gateways with: - - [Cursor](/guides/tool-calling/mcp-clients/cursor) - - [Visual Studio Code](/guides/tool-calling/mcp-clients/visual-studio-code) + - [Cursor](/get-started/mcp-clients/cursor) + - [Visual Studio Code](/get-started/mcp-clients/visual-studio-code) - Build your own MCP servers with [arcade-mcp](/get-started/quickstarts/mcp-server-quickstart). diff --git a/app/en/guides/_meta.tsx b/app/en/guides/_meta.tsx index bf2e1073b..3c8f69763 100644 --- a/app/en/guides/_meta.tsx +++ b/app/en/guides/_meta.tsx @@ -7,9 +7,6 @@ export const meta: MetaRecord = { "create-tools": { title: "Create tools", }, - "agent-frameworks": { - title: "Agent frameworks", - }, "user-facing-agents": { title: "User-facing agents", }, diff --git a/app/en/guides/create-tools/mcp-gateways/page.mdx b/app/en/guides/create-tools/mcp-gateways/page.mdx index c0d2288da..303c9befe 100644 --- a/app/en/guides/create-tools/mcp-gateways/page.mdx +++ b/app/en/guides/create-tools/mcp-gateways/page.mdx @@ -51,6 +51,6 @@ The options available when configuring an MCP Gateway are: Any MCP client that supports the Streamable HTTP transport can use an Arcade MCP Gateway. To use an Arcade MCP Gateway, you can use the `https://api.arcade.dev/mcp/` URL in your MCP client. Learn how to use MCP Gateways with: -- [Cursor](/guides/tool-calling/mcp-clients/cursor) -- [Claude Desktop](/guides/tool-calling/mcp-clients/claude-desktop) -- [Visual Studio Code](/guides/tool-calling/mcp-clients/visual-studio-code) +- [Cursor](/get-started/mcp-clients/cursor) +- [Claude Desktop](/get-started/mcp-clients/claude-desktop) +- [Visual Studio Code](/get-started/mcp-clients/visual-studio-code) diff --git a/app/en/guides/deployment-hosting/on-prem/page.mdx b/app/en/guides/deployment-hosting/on-prem/page.mdx index db008fbbd..1d46bbc71 100644 --- a/app/en/guides/deployment-hosting/on-prem/page.mdx +++ b/app/en/guides/deployment-hosting/on-prem/page.mdx @@ -284,9 +284,9 @@ You can now test your MCP Server by making requests using the Playground, or an 1. Use an app that supports MCP clients, like AI assistants and IDEs: - - [Visual Studio Code](/guides/tool-calling/mcp-clients/visual-studio-code) - - [Claude Desktop](/guides/tool-calling/mcp-clients/claude-desktop) - - [Cursor](/guides/tool-calling/mcp-clients/cursor) + - [Visual Studio Code](/get-started/mcp-clients/visual-studio-code) + - [Claude Desktop](/get-started/mcp-clients/claude-desktop) + - [Cursor](/get-started/mcp-clients/cursor) 1. Enable your MCP Server from the list of available MCP Servers 1. Verify that the response is correct and you see request logs in your MCP Server diff --git a/app/en/guides/tool-calling/_meta.tsx b/app/en/guides/tool-calling/_meta.tsx index bfef9282b..4afb60e1a 100644 --- a/app/en/guides/tool-calling/_meta.tsx +++ b/app/en/guides/tool-calling/_meta.tsx @@ -7,9 +7,6 @@ export const meta: MetaRecord = { "call-third-party-apis": { title: "Call third-party APIs", }, - "mcp-clients": { - title: "Connect to MCP clients", - }, "custom-apps": { title: "In custom applications", }, diff --git a/app/en/guides/tool-calling/mcp-clients/copilot-studio/page.mdx b/app/en/guides/tool-calling/mcp-clients/copilot-studio/page.mdx new file mode 100644 index 000000000..304827b5c --- /dev/null +++ b/app/en/guides/tool-calling/mcp-clients/copilot-studio/page.mdx @@ -0,0 +1,100 @@ +import { Steps } from "nextra/components"; +import { SignupLink } from "@/app/_components/analytics"; +import Image from "next/image"; + +export const IMAGE_SCALE_FACTOR = 2; +export const STEP_1_WIDTH = 1058; +export const STEP_1_HEIGHT = 462; +export const STEP_2_WIDTH = 1246; +export const STEP_2_HEIGHT = 902; +export const STEP_3_WIDTH = 924; +export const STEP_3_HEIGHT = 1074; +export const STEP_4_WIDTH = 1512; +export const STEP_4_HEIGHT = 790; + +# Use Arcade in Microsoft Copilot Studio + + + + +Connect Microsoft Copilot Studio to an Arcade MCP Gateway. + + + + + +1. A Microsoft 365 subscription with access to Copilot Studio +2. Create an Arcade account +3. Get an [Arcade API key](/get-started/setup/api-keys) +4. Create an [Arcade MCP Gateway](/guides/create-tools/mcp-gateways) and select the tools you want to use + + + + + + + +### Create or open your agent + +In [Copilot Studio](https://copilotstudio.microsoft.com/), create a new agent or open an existing one that you want to connect to Arcade tools. + +### Add a new MCP tool + +1. Inside your agent, click the **Tools** tab in the navigation panel +2. Click on **Add a tool** +3. In the Add tools panel, select **Model Context Protocol** +4. Click on **New tool** to configure a new MCP connection + +{"Step + +### Configure the MCP Gateway connection + +In the Model Context Protocol configuration dialog: + +1. Enter a **Server name** for your connection (for example, "PersonalAssistantTools") +2. Add a **Server description** to help identify the tools available +3. Enter your Arcade MCP Gateway URL in the **Server URL** field: `https://api.arcade.dev/mcp/` +4. Under **Authentication**, select **OAuth 2.0** +5. Under **Type**, select **Dynamic discovery** to authorize the MCP gateway automatically using OAuth + +{"Step + +### Complete the authorization flow + +After saving the gateway configuration, you'll be redirected to the Arcade authorization page. Review the permissions requested and click **Allow** to authorize Copilot Studio to access your MCP resources. + +{"Step + +### Start using your tools + +Once the connection is established, return to your agent and start a conversation. Now you can directly interact with your tools. + +Arcade provides just-in-time authorization. When you use a tool that requires access to an external service, Copilot Studio will display an authorization link. Click the link to grant access and continue. This works seamlessly with tools like SharePoint, Outlook, Teams, Stripe, and Gmail. + +{"Step + + diff --git a/app/en/home/landing-page.tsx b/app/en/home/landing-page.tsx index c98712df5..a6cca53bb 100644 --- a/app/en/home/landing-page.tsx +++ b/app/en/home/landing-page.tsx @@ -345,7 +345,7 @@ export function LandingPage() {
The `TrashEmail` tool is currently only available on a self-hosted instance of the Arcade Engine. To learn more about self-hosting, see the [self-hosting - documentation](http://localhost:3000/en/home/deployment/engine-configuration). + documentation](http://localhost:3000/en/guides/deployment-hosting/configure-engine).
diff --git a/next.config.ts b/next.config.ts index 4cd7690c0..23803aa95 100644 --- a/next.config.ts +++ b/next.config.ts @@ -27,25 +27,25 @@ const nextConfig: NextConfig = withLlmsTxt({ { source: "/:locale/home/langchain/use-arcade-tools", destination: - "/:locale/guides/agent-frameworks/langchain/use-arcade-tools", + "/:locale/get-started/agent-frameworks/langchain/use-arcade-tools", permanent: true, }, { source: "/:locale/home/langchain/user-auth-interrupts", destination: - "/:locale/guides/agent-frameworks/langchain/user-auth-interrupts", + "/:locale/get-started/agent-frameworks/langchain/user-auth-interrupts", permanent: true, }, { source: "/:locale/home/oai-agents/user-auth-interrupts", destination: - "/:locale/guides/agent-frameworks/openai-agents/user-auth-interrupts", + "/:locale/get-started/agent-frameworks/openai-agents/user-auth-interrupts", permanent: true, }, { source: "/:locale/home/mastra/user-auth-interrupts", destination: - "/:locale/guides/agent-frameworks/mastra/user-auth-interrupts", + "/:locale/get-started/agent-frameworks/mastra/user-auth-interrupts", permanent: true, }, { @@ -60,7 +60,7 @@ const nextConfig: NextConfig = withLlmsTxt({ }, { source: "/:locale/home/agent-frameworks-overview", - destination: "/:locale/guides/agent-frameworks", + destination: "/:locale/get-started/agent-frameworks", permanent: true, }, { @@ -76,7 +76,7 @@ const nextConfig: NextConfig = withLlmsTxt({ { source: "/:locale/guides/agent-frameworks/vercelai/using-arcade-tools", - destination: "/:locale/guides/agent-frameworks/vercelai", + destination: "/:locale/get-started/agent-frameworks/vercelai", permanent: true, }, { @@ -194,13 +194,13 @@ const nextConfig: NextConfig = withLlmsTxt({ { source: "/:locale/home/crewai/custom-auth-flow", destination: - "/:locale/guides/agent-frameworks/crewai/custom-auth-flow", + "/:locale/get-started/agent-frameworks/crewai/custom-auth-flow", permanent: true, }, { source: "/:locale/home/crewai/use-arcade-tools", destination: - "/:locale/guides/agent-frameworks/crewai/use-arcade-tools", + "/:locale/get-started/agent-frameworks/crewai/use-arcade-tools", permanent: true, }, { @@ -254,7 +254,7 @@ const nextConfig: NextConfig = withLlmsTxt({ { source: "/:locale/home/google-adk/use-arcade-tools", destination: - "/:locale/guides/agent-frameworks/google-adk/use-arcade-tools", + "/:locale/get-started/agent-frameworks/google-adk/use-arcade-tools", permanent: true, }, { @@ -265,30 +265,28 @@ const nextConfig: NextConfig = withLlmsTxt({ { source: "/:locale/home/langchain/auth-langchain-tools", destination: - "/:locale/guides/agent-frameworks/langchain/auth-langchain-tools", + "/:locale/get-started/agent-frameworks/langchain/auth-langchain-tools", permanent: true, }, { source: "/:locale/home/mastra/use-arcade-tools", destination: - "/:locale/guides/agent-frameworks/mastra/use-arcade-tools", + "/:locale/get-started/agent-frameworks/mastra/use-arcade-tools", permanent: true, }, { source: "/:locale/home/mcp-clients/claude-desktop", - destination: - "/:locale/guides/tool-calling/mcp-clients/claude-desktop", + destination: "/:locale/get-started/mcp-clients/claude-desktop", permanent: true, }, { source: "/:locale/home/mcp-clients/cursor", - destination: "/:locale/guides/tool-calling/mcp-clients/cursor", + destination: "/:locale/get-started/mcp-clients/cursor", permanent: true, }, { source: "/:locale/home/mcp-clients/visual-studio-code", - destination: - "/:locale/guides/tool-calling/mcp-clients/visual-studio-code", + destination: "/:locale/get-started/mcp-clients/visual-studio-code", permanent: true, }, { @@ -304,7 +302,7 @@ const nextConfig: NextConfig = withLlmsTxt({ { source: "/:locale/home/oai-agents/use-arcade-tools", destination: - "/:locale/guides/agent-frameworks/openai-agents/use-arcade-tools", + "/:locale/get-started/agent-frameworks/openai-agents/use-arcade-tools", permanent: true, }, { @@ -351,8 +349,7 @@ const nextConfig: NextConfig = withLlmsTxt({ }, { source: "/:locale/home/vercelai/using-arcade-tools", - destination: - "/:locale/guides/agent-frameworks/vercelai/using-arcade-tools", + destination: "/:locale/get-started/agent-frameworks/vercelai", permanent: true, }, // MCP servers to integrations @@ -409,7 +406,7 @@ const nextConfig: NextConfig = withLlmsTxt({ }, { source: "/:locale/guides/tool-calling/mcp-client/:client", - destination: "/:locale/guides/tool-calling/mcp-clients/:client", + destination: "/:locale/get-started/mcp-clients/:client", permanent: true, }, { @@ -439,55 +436,55 @@ const nextConfig: NextConfig = withLlmsTxt({ { source: "/:locale/guides/agent-frameworks/crewai/python", destination: - "/:locale/guides/agent-frameworks/crewai/use-arcade-tools", + "/:locale/get-started/agent-frameworks/crewai/use-arcade-tools", permanent: true, }, { source: "/:locale/guides/agent-frameworks/langchain/python", destination: - "/:locale/guides/agent-frameworks/langchain/use-arcade-tools", + "/:locale/get-started/agent-frameworks/langchain/use-arcade-tools", permanent: true, }, { source: "/:locale/guides/agent-frameworks/langchain/tools", destination: - "/:locale/guides/agent-frameworks/langchain/auth-langchain-tools", + "/:locale/get-started/agent-frameworks/langchain/auth-langchain-tools", permanent: true, }, { source: "/:locale/guides/agent-frameworks/mastra/typescript", destination: - "/:locale/guides/agent-frameworks/mastra/use-arcade-tools", + "/:locale/get-started/agent-frameworks/mastra/use-arcade-tools", permanent: true, }, { source: "/:locale/guides/agent-frameworks/google-adk/python", destination: - "/:locale/guides/agent-frameworks/google-adk/use-arcade-tools", + "/:locale/get-started/agent-frameworks/google-adk/use-arcade-tools", permanent: true, }, { source: "/:locale/guides/agent-frameworks/openai/python", destination: - "/:locale/guides/agent-frameworks/openai-agents/use-arcade-tools", + "/:locale/get-started/agent-frameworks/openai-agents/use-arcade-tools", permanent: true, }, { source: "/:locale/guides/agent-frameworks/vercel-ai/typescript", - destination: "/:locale/guides/agent-frameworks/vercelai", + destination: "/:locale/get-started/agent-frameworks/vercelai", permanent: true, }, // Old resource paths { source: "/:locale/resources/mastra/user-auth-interrupts", destination: - "/:locale/guides/agent-frameworks/mastra/user-auth-interrupts", + "/:locale/get-started/agent-frameworks/mastra/user-auth-interrupts", permanent: true, }, { source: "/:locale/resources/oai-agents/overview", destination: - "/:locale/guides/agent-frameworks/openai-agents/overview", + "/:locale/get-started/agent-frameworks/openai-agents/overview", permanent: true, }, { @@ -495,6 +492,28 @@ const nextConfig: NextConfig = withLlmsTxt({ destination: "/:locale/guides/create-tools/:path*", permanent: true, }, + // Agent frameworks moved from guides to get-started + { + source: "/:locale/guides/agent-frameworks", + destination: "/:locale/get-started/agent-frameworks", + permanent: true, + }, + { + source: "/:locale/guides/agent-frameworks/:path*", + destination: "/:locale/get-started/agent-frameworks/:path*", + permanent: true, + }, + // MCP clients moved from guides/tool-calling to get-started + { + source: "/:locale/guides/tool-calling/mcp-clients", + destination: "/:locale/get-started/mcp-clients", + permanent: true, + }, + { + source: "/:locale/guides/tool-calling/mcp-clients/:path*", + destination: "/:locale/get-started/mcp-clients/:path*", + permanent: true, + }, ]; }, headers: async () => [ diff --git a/public/images/icons/microsoft-copilot-studio.png b/public/images/icons/microsoft-copilot-studio.png new file mode 100644 index 000000000..ae55ede97 Binary files /dev/null and b/public/images/icons/microsoft-copilot-studio.png differ diff --git a/public/images/mcp-gateway/copilot-studio/step-1.png b/public/images/mcp-gateway/copilot-studio/step-1.png new file mode 100644 index 000000000..4a0f43ec8 Binary files /dev/null and b/public/images/mcp-gateway/copilot-studio/step-1.png differ diff --git a/public/images/mcp-gateway/copilot-studio/step-2.png b/public/images/mcp-gateway/copilot-studio/step-2.png new file mode 100644 index 000000000..62a419321 Binary files /dev/null and b/public/images/mcp-gateway/copilot-studio/step-2.png differ diff --git a/public/images/mcp-gateway/copilot-studio/step-3.png b/public/images/mcp-gateway/copilot-studio/step-3.png new file mode 100644 index 000000000..ca8c43182 Binary files /dev/null and b/public/images/mcp-gateway/copilot-studio/step-3.png differ diff --git a/public/images/mcp-gateway/copilot-studio/step-4.png b/public/images/mcp-gateway/copilot-studio/step-4.png new file mode 100644 index 000000000..0294c6694 Binary files /dev/null and b/public/images/mcp-gateway/copilot-studio/step-4.png differ diff --git a/public/llms.txt b/public/llms.txt index 39f89d763..9ad1de7c7 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -99,6 +99,7 @@ Arcade delivers three core capabilities: Deploy agents even your security team w - [Comparative evaluations](https://docs.arcade.dev/en/guides/create-tools/evaluate-tools/comparative-evaluations.md): The "Comparative Evaluations" documentation page guides users in testing and comparing the performance of AI models across different tool implementations using isolated tool registries, known as tracks. It outlines how to set up comparative evaluations, register tools, create test cases, - [Compare MCP Server Types](https://docs.arcade.dev/en/guides/create-tools/tool-basics/compare-server-types.md): This documentation page provides a comparative overview of different MCP server types offered by Arcade, detailing their functionalities based on transport method and deployment options. Users can learn about the capabilities of each server type, including the availability of tools with or without authentication and secrets. - [Confluence](https://docs.arcade.dev/en/resources/integrations/productivity/confluence.md): This documentation page provides a comprehensive overview of the Arcade Confluence MCP Server, which enables users to build agents and AI applications that interact with Confluence. It details various tools available for managing pages, spaces, and attachments, as well as searching for content +- [Connect Arcade to your LLM](https://docs.arcade.dev/en/guides/agent-frameworks/setup-arcade-with-your-llm-python.md): This documentation page provides a step-by-step guide for integrating Arcade's tool-calling capabilities into a Python application that utilizes a Large Language Model (LLM). Users will learn how to create a harness to facilitate interactions between the user, the model, and - [Connect to MCP Clients](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients.md): This documentation page provides guidance on connecting Arcade MCP servers to various MCP-compatible clients and development environments, enabling users to enhance their agent workflows. - [Contact Us](https://docs.arcade.dev/en/resources/contact-us.md): This documentation page provides users with information on how to connect with the Arcade team for support through various channels. It aims to facilitate communication and assistance for users and their agents. - [Create a new Mastra project](https://docs.arcade.dev/en/guides/agent-frameworks/mastra/use-arcade-tools.md): This documentation page provides a step-by-step guide for integrating Arcade tools into a new Mastra project, enabling users to leverage these tools within their Mastra applications. It covers prerequisites, project setup, installation of the Arcade client, API key configuration, and @@ -237,6 +238,7 @@ Arcade delivers three core capabilities: Deploy agents even your security team w - [Types of Tools](https://docs.arcade.dev/en/guides/create-tools/improve/types-of-tools.md): This documentation page explains the two types of tools offered by Arcade: Optimized tools and Starter tools. It highlights the differences in design and functionality, emphasizing that Optimized tools are tailored for AI-powered chat interfaces to improve performance, while Starter tools provide more - [Understanding `Context` and tools](https://docs.arcade.dev/en/guides/create-tools/tool-basics/runtime-data-access.md): This documentation page explains the `Context` class used in Arcade tools, detailing how to access runtime capabilities and tool-specific data securely. Users will learn how to utilize the `Context` object to retrieve OAuth tokens, secrets, user information, and to log - [Use Arcade in Cursor](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients/cursor.md): This documentation page provides a step-by-step guide for users to connect Cursor to an Arcade MCP Gateway, enabling the use of Arcade tools within Cursor. It outlines the prerequisites needed for setup, including creating an Arcade account and obtaining an API key, as well +- [Use Arcade in Microsoft Copilot Studio](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients/copilot-studio.md): This documentation page guides users on how to connect Microsoft Copilot Studio to an Arcade MCP Gateway, enabling the integration of Arcade tools within their agents. It outlines the prerequisites, step-by-step instructions for creating or opening an agent, adding an MCP tool, - [Use Arcade in Visual Studio Code](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients/visual-studio-code.md): This documentation page provides a step-by-step guide for connecting Visual Studio Code to an Arcade MCP Gateway, enabling users to integrate and utilize Arcade tools within the IDE. It outlines the prerequisites for setup, including creating an Arcade account and obtaining an API key, - [Use Arcade with Claude Desktop](https://docs.arcade.dev/en/guides/tool-calling/mcp-clients/claude-desktop.md): This documentation page is intended to guide users on how to utilize Arcade with Claude Desktop. However, it currently indicates that the content is forthcoming and does not provide any detailed instructions or information at this time. - [VercelApi](https://docs.arcade.dev/en/resources/integrations/development/vercel-api.md): The VercelApi documentation provides a comprehensive guide for users to manage their Vercel projects, domains, and integrations through various API tools. It outlines available functionalities such as creating and managing access groups, handling deployments, and managing DNS records, enabling diff --git a/scripts/check-redirects.sh b/scripts/check-redirects.sh new file mode 100755 index 000000000..6a5cedb1a --- /dev/null +++ b/scripts/check-redirects.sh @@ -0,0 +1,523 @@ +#!/bin/bash +# +# Check that deleted markdown files have corresponding redirects in next.config.ts +# Usage: ./scripts/check-redirects.sh [--auto-fix] [base_branch] +# +# This script compares the current branch to main (or specified base branch) +# and ensures any deleted .md/.mdx files have redirect entries. +# +# Options: +# --auto-fix Automatically add missing redirects to next.config.ts +# +# Features: +# - Detects deleted markdown files without redirects +# - Interactive mode: prompts for redirect destinations when run in a terminal +# - Auto-fix mode: automatically inserts redirect entries into next.config.ts +# - Validates existing redirects for circular references and invalid destinations +# + +set -e + +# Parse arguments +AUTO_FIX=false +BASE_BRANCH="main" + +for arg in "$@"; do + case $arg in + --auto-fix) + AUTO_FIX=true + shift + ;; + *) + BASE_BRANCH="$arg" + shift + ;; + esac +done + +CONFIG_FILE="next.config.ts" +EXIT_CODE=0 + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Check if running interactively +IS_INTERACTIVE=false +if [ -t 0 ] && [ -t 1 ]; then + IS_INTERACTIVE=true +fi + +echo "Checking for deleted markdown files without redirects..." +echo "Comparing current branch to: $BASE_BRANCH" +echo "" + +# Ensure we have the base branch available for comparison +if ! git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then + echo "Fetching $BASE_BRANCH branch..." + if ! git fetch origin "$BASE_BRANCH:$BASE_BRANCH" 2>/dev/null; then + echo -e "${RED}ERROR: Could not fetch base branch '$BASE_BRANCH'${NC}" + echo "Please ensure you have network access and the branch exists." + exit 1 + fi +fi + +# Verify base branch is now available +if ! git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then + echo -e "${RED}ERROR: Base branch '$BASE_BRANCH' is not available${NC}" + echo "Cannot compare branches. Please check your git configuration." + exit 1 +fi + +# Verify we can actually diff against the base branch +# This catches cases where rev-parse succeeds but diff fails (unrelated histories, corrupted refs, etc.) +if ! git diff --name-status "$BASE_BRANCH"...HEAD >/dev/null 2>&1; then + echo -e "${RED}ERROR: Cannot compare current branch to '$BASE_BRANCH'${NC}" + echo "The branches may have unrelated histories or the ref may be corrupted." + echo "Try running: git fetch origin $BASE_BRANCH:$BASE_BRANCH" + exit 1 +fi + +# Get list of deleted/renamed page files (comparing to base branch) +# Include both committed changes and uncommitted working tree changes +# D = deleted, R = renamed (old path needs redirect too) +# Only match page.md or page.mdx files (actual routable pages in Next.js App Router) +# For renames (R100old-pathnew-path), we need to check the OLD path (field 2), +# not the new path. Extract field 2 first, then filter for page files. +# Note: The diff command was already validated above, so failures here are from the grep/cut pipeline +# which legitimately returns empty when no files match (hence || true is safe here) +COMMITTED_DELETES=$(git diff --name-status "$BASE_BRANCH"...HEAD | grep -E "^D|^R" | cut -f2 | grep -E 'page\.(md|mdx)$' || true) +UNCOMMITTED_DELETES=$(git diff --name-status HEAD | grep -E "^D|^R" | cut -f2 | grep -E 'page\.(md|mdx)$' || true) + +# Combine and deduplicate +DELETED_FILES=$(echo -e "${COMMITTED_DELETES}\n${UNCOMMITTED_DELETES}" | sort -u | grep -v '^$' || true) + +# Function to convert file path to URL path +# e.g., app/en/guides/foo/page.mdx -> /:locale/guides/foo +# e.g., app/en/page.mdx -> /:locale (root page) +file_to_url() { + local file_path="$1" + # Remove app/en/ prefix and page.mdx or page.md suffix (with or without leading /) + # Using separate sed commands for portability (BSD vs GNU sed) + local url_path=$(echo "$file_path" | sed -E 's|^app/[a-z]{2}/||' | sed -E 's|/?page\.mdx$||' | sed -E 's|/?page\.md$||') + # Handle root page case (empty url_path after stripping) + if [ -z "$url_path" ]; then + echo "/:locale" + else + echo "/:locale/$url_path" + fi +} + +# Function to convert URL path to file path for validation +# e.g., /:locale/guides/foo -> app/en/guides/foo/page.mdx +url_to_file() { + local url_path="$1" + # Remove /:locale/ prefix and add app/en/ prefix + local file_path=$(echo "$url_path" | sed 's|^/:locale/||') + echo "app/en/$file_path/page.mdx" +} + +# Function to check if a page exists (considering both new and existing files) +page_exists() { + local url_path="$1" + local file_path=$(url_to_file "$url_path") + + # Check if file exists on disk + if [ -f "$file_path" ]; then + return 0 + fi + + # Also check for .md extension + local md_path=$(echo "$file_path" | sed 's|\.mdx$|.md|') + if [ -f "$md_path" ]; then + return 0 + fi + + # Check if it's a wildcard destination (valid by definition) + if [[ "$url_path" == *":path*"* ]]; then + return 0 + fi + + return 1 +} + +# Read the config file content +CONFIG_CONTENT=$(cat "$CONFIG_FILE" 2>/dev/null || echo "") + +# Extract redirect pairs using a more robust method +# Join lines and extract source/destination pairs +REDIRECT_PAIRS=$(cat "$CONFIG_FILE" | tr '\n' ' ' | grep -oE '\{[^}]*source:[^}]*destination:[^}]*\}' || true) + +# Function to find where a path redirects to (if it has a redirect) +find_redirect_destination() { + local path="$1" + echo "$REDIRECT_PAIRS" | while read -r block; do + [ -z "$block" ] && continue + local src=$(echo "$block" | sed -n 's/.*source:[[:space:]]*"\([^"]*\)".*/\1/p') + if [ "$src" = "$path" ]; then + echo "$block" | sed -n 's/.*destination:[[:space:]]*"\([^"]*\)".*/\1/p' + return 0 + fi + done +} + +# Arrays to track redirect chains that need collapsing +declare -a CHAIN_SOURCES=() +declare -a CHAIN_OLD_DESTS=() +declare -a CHAIN_NEW_DESTS=() + +# ============================================================ +# PART 1: Validate existing redirects in the config +# ============================================================ +echo -e "${BLUE}Validating existing redirects in $CONFIG_FILE...${NC}" +echo "" + +declare -a INVALID_REDIRECTS=() + +while IFS= read -r redirect_block; do + [ -z "$redirect_block" ] && continue + + # Extract source and destination from the block + source_path=$(echo "$redirect_block" | sed -n 's/.*source:[[:space:]]*"\([^"]*\)".*/\1/p') + dest_path=$(echo "$redirect_block" | sed -n 's/.*destination:[[:space:]]*"\([^"]*\)".*/\1/p') + + [ -z "$source_path" ] && continue + [ -z "$dest_path" ] && continue + + # Check for placeholder text + if [[ "$dest_path" == *"REPLACE_WITH"* ]] || [[ "$dest_path" == *"TODO"* ]] || [[ "$dest_path" == *"FIXME"* ]]; then + echo -e "${RED}āœ— Invalid redirect: $source_path${NC}" + echo -e " ${YELLOW}Destination contains placeholder text: $dest_path${NC}" + INVALID_REDIRECTS+=("$source_path -> $dest_path (placeholder text)") + EXIT_CODE=1 + # Check for circular redirect + elif [ "$source_path" = "$dest_path" ]; then + echo -e "${RED}āœ— Circular redirect: $source_path${NC}" + echo -e " ${YELLOW}Source and destination are the same!${NC}" + INVALID_REDIRECTS+=("$source_path -> $dest_path (circular)") + EXIT_CODE=1 + # Check if destination exists (skip wildcards and other dynamic segments) + # Skip paths with :path* or :path wildcards + # Skip paths with dynamic segments OTHER than :locale (check after stripping /:locale/) + elif [[ "$dest_path" != *":path*"* ]] && [[ "$dest_path" != *":path"* ]]; then + # Strip the /:locale/ prefix and check for remaining dynamic segments + dest_without_locale=$(echo "$dest_path" | sed 's|^/:locale/||') + if [[ "$dest_without_locale" != *":"* ]]; then + if ! page_exists "$dest_path"; then + # Check if the destination has its own redirect (chain situation) + final_dest=$(find_redirect_destination "$dest_path") + if [ -n "$final_dest" ]; then + # This is a redirect chain - track it for collapsing + echo -e "${YELLOW}⚠ Redirect chain detected: $source_path${NC}" + echo -e " ${BLUE}$source_path → $dest_path → $final_dest${NC}" + CHAIN_SOURCES+=("$source_path") + CHAIN_OLD_DESTS+=("$dest_path") + CHAIN_NEW_DESTS+=("$final_dest") + else + echo -e "${RED}āœ— Invalid redirect: $source_path${NC}" + echo -e " ${YELLOW}Destination does not exist: $dest_path${NC}" + INVALID_REDIRECTS+=("$source_path -> $dest_path (destination not found)") + EXIT_CODE=1 + fi + fi + fi + fi +done <<< "$REDIRECT_PAIRS" + +if [ ${#INVALID_REDIRECTS[@]} -eq 0 ] && [ ${#CHAIN_SOURCES[@]} -eq 0 ]; then + echo -e "${GREEN}āœ“ All existing redirects are valid.${NC}" +fi +echo "" + +# ============================================================ +# PART 1b: Auto-fix redirect chains (collapse them) +# ============================================================ +if [ ${#CHAIN_SOURCES[@]} -gt 0 ]; then + if [ "$AUTO_FIX" = true ]; then + echo -e "${BLUE}Collapsing ${#CHAIN_SOURCES[@]} redirect chain(s)...${NC}" + echo "" + + for i in "${!CHAIN_SOURCES[@]}"; do + src="${CHAIN_SOURCES[$i]}" + old_dest="${CHAIN_OLD_DESTS[$i]}" + new_dest="${CHAIN_NEW_DESTS[$i]}" + + echo -e " ${GREEN}āœ“${NC} $src" + echo -e " was: $old_dest" + echo -e " now: $new_dest" + + # Escape special characters for sed + old_dest_escaped=$(printf '%s\n' "$old_dest" | sed 's/[[\.*^$()+?{|/]/\\&/g') + new_dest_escaped=$(printf '%s\n' "$new_dest" | sed 's/[[\.*^$()+?{|/]/\\&/g') + + # Replace the old destination with the new one in the config + sed -i.bak "s|destination: \"$old_dest_escaped\"|destination: \"$new_dest_escaped\"|g" "$CONFIG_FILE" + rm -f "$CONFIG_FILE.bak" + done + echo "" + echo -e "${GREEN}āœ“ Redirect chains collapsed in $CONFIG_FILE${NC}" + echo "" + else + echo -e "${YELLOW}Found ${#CHAIN_SOURCES[@]} redirect chain(s) that need collapsing.${NC}" + echo "Run with --auto-fix to collapse them automatically." + echo "" + EXIT_CODE=1 + fi +fi + +# ============================================================ +# PART 2: Check for deleted files without redirects +# ============================================================ + +if [ -z "$DELETED_FILES" ]; then + echo -e "${GREEN}āœ“ No deleted markdown files found.${NC}" + exit $EXIT_CODE +fi + +echo "Found deleted markdown files:" +echo "$DELETED_FILES" +echo "" + +# Check if next.config.ts was modified (committed or uncommitted) +CONFIG_MODIFIED_COMMITTED=$(git diff --name-only "$BASE_BRANCH"...HEAD 2>/dev/null | grep -c "$CONFIG_FILE" || true) +CONFIG_MODIFIED_UNCOMMITTED=$(git diff --name-only HEAD 2>/dev/null | grep -c "$CONFIG_FILE" || true) +CONFIG_MODIFIED=$((${CONFIG_MODIFIED_COMMITTED:-0} + ${CONFIG_MODIFIED_UNCOMMITTED:-0})) + +# Arrays to track missing redirects +declare -a MISSING_REDIRECTS=() +declare -a SUGGESTED_ENTRIES=() + +# Function to check if a wildcard redirect covers this path +check_wildcard_match() { + local path="$1" + # Extract path segments (remove /:locale/ prefix for matching) + local path_without_locale=$(echo "$path" | sed 's|^/:locale/||') + + # Look for wildcard patterns that could match + # Pattern: source: "/:locale/some/path/:path*" + while IFS= read -r line; do + # Extract the source pattern + local source_pattern=$(echo "$line" | sed -n 's/.*source:.*"\(.*\)".*/\1/p') + [ -z "$source_pattern" ] && continue + + # Check if it's a wildcard pattern + if [[ "$source_pattern" == *":path*"* ]]; then + # Convert wildcard to prefix (remove :path* and everything after) + local prefix=$(echo "$source_pattern" | sed 's|/:path\*.*||' | sed 's|^/:locale/||') + + # Check if our path starts with this prefix + if [[ "$path_without_locale" == "$prefix"/* ]] || [[ "$path_without_locale" == "$prefix" ]]; then + return 0 # Match found + fi + fi + done <<< "$(echo "$CONFIG_CONTENT" | grep 'source:')" + + return 1 # No match +} + +# Function to prompt for a destination path +prompt_for_destination() { + local source_path="$1" + local destination="" + + # All informational messages go to stderr so only the destination goes to stdout + echo "" >&2 + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2 + echo -e "${YELLOW}Redirect needed for: $source_path${NC}" >&2 + echo "" >&2 + echo "Where should this URL redirect to?" >&2 + echo " - Enter a path like: /:locale/get-started/quickstarts" >&2 + echo " - Or press Enter to skip (you'll need to add it manually)" >&2 + echo "" >&2 + read -p "> " destination + + if [ -z "$destination" ]; then + echo -e "${YELLOW}Skipped. You'll need to add this redirect manually.${NC}" >&2 + return 1 + fi + + # Validate the destination + if [[ "$destination" != "/:locale/"* ]]; then + echo -e "${YELLOW}Note: Adding /:locale/ prefix to your path${NC}" >&2 + destination="/:locale/$destination" + fi + + # Check if destination exists + if ! page_exists "$destination"; then + echo -e "${YELLOW}Warning: Destination '$destination' does not appear to exist.${NC}" >&2 + read -p "Use it anyway? (y/n) " confirm + if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then + return 1 + fi + fi + + # Return the destination (only this goes to stdout for capture) + echo "$destination" + return 0 +} + +# Check each deleted file +while IFS= read -r deleted_file; do + [ -z "$deleted_file" ] && continue + + # Only check files in the app directory (actual page files) + if [[ ! "$deleted_file" =~ ^app/ ]]; then + continue + fi + + # Convert to URL path + url_path=$(file_to_url "$deleted_file") + + # Create a search pattern for the source in redirects + search_pattern=$(echo "$url_path" | sed 's/[[\.*^$()+?{|]/\\&/g') + + # Check if this path exists as a redirect source in the config (exact or wildcard) + if echo "$CONFIG_CONTENT" | grep -q "source:.*\"$search_pattern\""; then + echo -e "${GREEN}āœ“ Redirect exists for: $url_path${NC}" + elif check_wildcard_match "$url_path"; then + echo -e "${GREEN}āœ“ Redirect exists for: $url_path (via wildcard)${NC}" + else + echo -e "${RED}āœ— Missing redirect for: $url_path${NC}" + MISSING_REDIRECTS+=("$url_path") + + # If interactive, prompt for destination + if [ "$IS_INTERACTIVE" = true ]; then + destination=$(prompt_for_destination "$url_path") + if [ $? -eq 0 ] && [ -n "$destination" ]; then + SUGGESTED_ENTRIES+=(" { + source: \"$url_path\", + destination: \"$destination\", + permanent: true, + },") + else + SUGGESTED_ENTRIES+=(" { + source: \"$url_path\", + destination: \"/:locale/REPLACE_WITH_NEW_PATH\", + permanent: true, + },") + fi + else + # Non-interactive: use placeholder + SUGGESTED_ENTRIES+=(" { + source: \"$url_path\", + destination: \"/:locale/REPLACE_WITH_NEW_PATH\", + permanent: true, + },") + fi + + EXIT_CODE=1 + fi +done <<< "$DELETED_FILES" + +echo "" + +# Report results and optionally auto-fix +if [ ${#MISSING_REDIRECTS[@]} -gt 0 ]; then + if [ "$AUTO_FIX" = true ]; then + echo -e "${BLUE}══════════════════════════════════════════════════════════════${NC}" + echo -e "${BLUE}Auto-fixing: Adding ${#MISSING_REDIRECTS[@]} redirect(s) to $CONFIG_FILE${NC}" + echo -e "${BLUE}══════════════════════════════════════════════════════════════${NC}" + echo "" + + # Write redirect entries to a temp file + TEMP_REDIRECTS=$(mktemp) + echo " // Auto-added redirects for deleted pages" > "$TEMP_REDIRECTS" + for entry in "${SUGGESTED_ENTRIES[@]}"; do + echo "$entry" >> "$TEMP_REDIRECTS" + done + + # Insert after "return [" in the redirects function using awk + TEMP_CONFIG=$(mktemp) + awk -v insertfile="$TEMP_REDIRECTS" ' + /return \[/ && !done { + print + while ((getline line < insertfile) > 0) print line + close(insertfile) + done = 1 + next + } + { print } + ' "$CONFIG_FILE" > "$TEMP_CONFIG" + + # Replace original with modified version + mv "$TEMP_CONFIG" "$CONFIG_FILE" + rm -f "$TEMP_REDIRECTS" + + echo -e "${GREEN}āœ“ Added redirect entries to $CONFIG_FILE${NC}" + echo "" + echo -e "${RED}══════════════════════════════════════════════════════════════${NC}" + echo -e "${RED}ACTION REQUIRED: Update placeholder destinations!${NC}" + echo -e "${RED}══════════════════════════════════════════════════════════════${NC}" + echo "" + echo "Redirect entries were added with placeholder destinations." + echo "You MUST update 'REPLACE_WITH_NEW_PATH' with actual paths before committing." + echo "" + echo -e "${YELLOW}Redirects needing destinations:${NC}" + for path in "${MISSING_REDIRECTS[@]}"; do + echo " - $path -> /:locale/REPLACE_WITH_NEW_PATH" + done + echo "" + echo "Open $CONFIG_FILE and search for 'REPLACE_WITH_NEW_PATH' to find them." + echo "" + + # Keep exit code as failure - placeholders must be replaced + EXIT_CODE=1 + else + echo -e "${RED}══════════════════════════════════════════════════════════════${NC}" + echo -e "${RED}ERROR: Found ${#MISSING_REDIRECTS[@]} deleted file(s) without redirects!${NC}" + echo -e "${RED}══════════════════════════════════════════════════════════════${NC}" + echo "" + echo "When you delete a markdown file, you must add a redirect in next.config.ts" + echo "to prevent broken links for users who have bookmarked the old URL." + echo "" + echo -e "${YELLOW}Missing redirects for:${NC}" + for path in "${MISSING_REDIRECTS[@]}"; do + echo " - $path" + done + echo "" + echo -e "${YELLOW}Add the following to the redirects array in next.config.ts:${NC}" + echo "" + for entry in "${SUGGESTED_ENTRIES[@]}"; do + echo "$entry" + done + echo "" + + if [ "$IS_INTERACTIVE" = false ]; then + echo "Replace 'REPLACE_WITH_NEW_PATH' with the actual destination path." + echo "If the content was removed entirely, redirect to a relevant parent page." + echo "" + fi + + if [ "$CONFIG_MODIFIED" -eq "0" ]; then + echo -e "${YELLOW}Note: next.config.ts was not modified in this branch.${NC}" + fi + fi +fi + +if [ ${#INVALID_REDIRECTS[@]} -gt 0 ]; then + echo "" + echo -e "${RED}══════════════════════════════════════════════════════════════${NC}" + echo -e "${RED}ERROR: Found ${#INVALID_REDIRECTS[@]} invalid redirect(s) in config!${NC}" + echo -e "${RED}══════════════════════════════════════════════════════════════${NC}" + echo "" + for invalid in "${INVALID_REDIRECTS[@]}"; do + echo " - $invalid" + done + echo "" + echo -e "${YELLOW}How to fix:${NC}" + echo " 1. Open next.config.ts" + echo " 2. Find the redirect(s) listed above" + echo " 3. Update the destination to a valid page path" + echo " (Check that the path exists under app/en/)" +fi + +if [ $EXIT_CODE -eq 0 ]; then + echo -e "${GREEN}══════════════════════════════════════════════════════════════${NC}" + echo -e "${GREEN}SUCCESS: All redirects are valid!${NC}" + echo -e "${GREEN}══════════════════════════════════════════════════════════════${NC}" +fi + +exit $EXIT_CODE diff --git a/scripts/update-internal-links.sh b/scripts/update-internal-links.sh new file mode 100755 index 000000000..c9160365b --- /dev/null +++ b/scripts/update-internal-links.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# +# Update internal links to use redirect destinations instead of old paths +# Usage: ./scripts/update-internal-links.sh [--dry-run] +# +# This script reads redirects from next.config.ts and updates any internal links +# in MDX/TSX files that point to redirected paths. +# +# Options: +# --dry-run Show what would be changed without making changes +# + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +DRY_RUN=false +if [[ "$1" == "--dry-run" ]]; then + DRY_RUN=true + echo -e "${BLUE}Running in dry-run mode - no files will be modified${NC}" + echo "" +fi + +CONFIG_FILE="next.config.ts" +UPDATED_COUNT=0 + +# Temporary file for storing redirects +REDIRECTS_FILE=$(mktemp) +trap "rm -f $REDIRECTS_FILE" EXIT + +echo -e "${BLUE}Parsing redirects from $CONFIG_FILE...${NC}" + +# Extract redirect pairs from next.config.ts +# Format: source|destination (one per line) +cat "$CONFIG_FILE" | tr '\n' ' ' | grep -oE '\{[^}]*source:[^}]*destination:[^}]*\}' | while read -r redirect_block; do + source_path=$(echo "$redirect_block" | sed -n 's/.*source:[[:space:]]*"\([^"]*\)".*/\1/p') + dest_path=$(echo "$redirect_block" | sed -n 's/.*destination:[[:space:]]*"\([^"]*\)".*/\1/p') + + # Skip if either is empty + [ -z "$source_path" ] && continue + [ -z "$dest_path" ] && continue + + # Skip wildcard redirects (can't auto-update these) + [[ "$source_path" == *":path*"* ]] && continue + [[ "$dest_path" == *":path*"* ]] && continue + + # Skip redirects with other dynamic segments (except :locale) + source_without_locale=$(echo "$source_path" | sed 's|^/:locale||') + dest_without_locale=$(echo "$dest_path" | sed 's|^/:locale||') + [[ "$source_without_locale" == *":"* ]] && continue + [[ "$dest_without_locale" == *":"* ]] && continue + + # Skip placeholder destinations + [[ "$dest_path" == *"REPLACE_WITH"* ]] && continue + [[ "$dest_path" == *"TODO"* ]] && continue + + # Store the redirect (without :locale prefix for matching) + echo "${source_without_locale}|${dest_without_locale}" >> "$REDIRECTS_FILE" +done + +REDIRECT_COUNT=$(wc -l < "$REDIRECTS_FILE" | tr -d ' ') +echo -e "Found ${GREEN}$REDIRECT_COUNT${NC} non-wildcard redirects to check" +echo "" + +if [ "$REDIRECT_COUNT" -eq 0 ]; then + echo -e "${GREEN}No redirects to process.${NC}" + exit 0 +fi + +echo -e "${BLUE}Scanning files for internal links to update...${NC}" +echo "" + +# Find all MDX and TSX files in app directory +FILES=$(find app -type f \( -name "*.mdx" -o -name "*.tsx" -o -name "*.md" \) 2>/dev/null) + +for file in $FILES; do + file_modified=false + + # Use a temp file for modifications + temp_file=$(mktemp) + cp "$file" "$temp_file" + + while IFS='|' read -r source dest; do + [ -z "$source" ] && continue + + # Simple check: does this file contain the source path? + if grep -q "$source" "$temp_file"; then + # Use perl for more reliable cross-platform replacement + # Replace in these contexts: + # ](/source) -> ](/dest) + # ](/source# -> ](/dest# + # ](/source" -> ](/dest" + # ](/source -> ](/dest (space for titles) + # href="/source" -> href="/dest" + # href='/source' -> href='/dest' + # href="/source# -> href="/dest# + + # Escape special regex chars in source for perl + source_perl=$(printf '%s' "$source" | sed 's/[[\.*^$()+?{|\\]/\\&/g') + dest_perl=$(printf '%s' "$dest" | sed 's/[&\\]/\\&/g') + + # Perform replacement with perl (handles all link patterns) + perl -i -pe "s|\Q${source}\E(?=[\"')\s#?])|${dest}|g" "$temp_file" 2>/dev/null || { + # Fallback to sed if perl fails + # Markdown links: ](source) -> ](dest) + sed -i.bak "s|](${source})|](${dest})|g" "$temp_file" 2>/dev/null || true + sed -i.bak "s|](${source}#|](${dest}#|g" "$temp_file" 2>/dev/null || true + sed -i.bak "s|](${source}\")|](${dest}\")|g" "$temp_file" 2>/dev/null || true + + # JSX href + sed -i.bak "s|href=\"${source}\"|href=\"${dest}\"|g" "$temp_file" 2>/dev/null || true + sed -i.bak "s|href='${source}'|href='${dest}'|g" "$temp_file" 2>/dev/null || true + sed -i.bak "s|href=\"${source}#|href=\"${dest}#|g" "$temp_file" 2>/dev/null || true + + rm -f "$temp_file.bak" 2>/dev/null || true + } + + file_modified=true + fi + done < "$REDIRECTS_FILE" + + # Check if file was actually modified + if [ "$file_modified" = true ] && ! diff -q "$file" "$temp_file" > /dev/null 2>&1; then + if [ "$DRY_RUN" = true ]; then + echo -e "${YELLOW}Would update:${NC} $file" + diff "$file" "$temp_file" | head -30 || true + echo "" + else + cp "$temp_file" "$file" + echo -e "${GREEN}Updated:${NC} $file" + fi + ((UPDATED_COUNT++)) || true + fi + + # Cleanup + rm -f "$temp_file" "$temp_file.bak" 2>/dev/null || true +done + +echo "" +echo -e "${BLUE}========================================================${NC}" +if [ "$DRY_RUN" = true ]; then + echo -e "${YELLOW}Dry run complete: $UPDATED_COUNT file(s) would be updated${NC}" + echo -e "Run without --dry-run to apply changes." +else + if [ "$UPDATED_COUNT" -gt 0 ]; then + echo -e "${GREEN}Updated $UPDATED_COUNT file(s) with new link paths${NC}" + else + echo -e "${GREEN}All internal links are up to date!${NC}" + fi +fi +echo -e "${BLUE}========================================================${NC}"