Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions app/_components/scope-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client";

import { useState } from "react";

interface Tool {
name: string;
scopes: string[];
}

interface ScopePickerProps {
tools: Tool[];
}

export default function ScopePicker({ tools }: ScopePickerProps) {
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());

const toggleTool = (toolName: string) => {
const newSelected = new Set(selectedTools);
if (newSelected.has(toolName)) {
newSelected.delete(toolName);
} else {
newSelected.add(toolName);
}
setSelectedTools(newSelected);
};

const selectAll = () => {
setSelectedTools(new Set(tools.map((t) => t.name)));
};

const clearAll = () => {
setSelectedTools(new Set());
};

// Get unique scopes from selected tools
const requiredScopes = Array.from(
new Set(
tools.filter((t) => selectedTools.has(t.name)).flatMap((t) => t.scopes)
)
).sort();

return (
<div className="my-6 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<div className="border-gray-200 border-b bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-800">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-gray-900 text-sm dark:text-gray-100">
Scope calculator
</h3>
<div className="flex gap-2">
<button
className="rounded bg-blue-100 px-2 py-1 text-blue-700 text-xs transition-colors hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300 dark:hover:bg-blue-800"
onClick={selectAll}
>
Select all
</button>
<button
className="rounded bg-gray-100 px-2 py-1 text-gray-600 text-xs transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
onClick={clearAll}
>
Clear
</button>
</div>
</div>
<p className="mt-1 text-gray-500 text-xs dark:text-gray-400">
Select the tools you plan to use to see the required OAuth scopes.
</p>
</div>

<div className="p-4">
<div className="mb-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
{tools.map((tool) => (
<label
className={`flex cursor-pointer items-center gap-2 rounded p-2 transition-colors ${
selectedTools.has(tool.name)
? "border border-blue-200 bg-blue-50 dark:border-blue-700 dark:bg-blue-900/30"
: "border border-gray-200 bg-gray-50 hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
}`}
key={tool.name}
>
<input
checked={selectedTools.has(tool.name)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600"
onChange={() => toggleTool(tool.name)}
type="checkbox"
/>
<span className="text-gray-700 text-sm dark:text-gray-300">
{tool.name}
</span>
</label>
))}
</div>

<div className="border-gray-200 border-t pt-4 dark:border-gray-700">
<h4 className="mb-2 font-medium text-gray-900 text-sm dark:text-gray-100">
Required scopes{" "}
{selectedTools.size > 0 && (
<span className="font-normal text-gray-500 dark:text-gray-400">
({requiredScopes.length})
</span>
)}
</h4>
{requiredScopes.length > 0 ? (
<ul className="space-y-1">
{requiredScopes.map((scope) => (
<li
className="rounded bg-gray-100 px-2 py-1 font-mono text-gray-700 text-xs dark:bg-gray-800 dark:text-gray-300"
key={scope}
>
{scope}
</li>
))}
</ul>
) : (
<p className="text-gray-500 text-sm italic dark:text-gray-400">
Select tools above to see required scopes
</p>
)}
</div>
</div>
</div>
);
}
54 changes: 53 additions & 1 deletion app/en/home/auth-providers/google/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,57 @@ This auth provider is used by:

## Configuring Google auth

You can either use Arcade's default Google OAuth provider (fastest way to get started), or configure your own Google OAuth provider for production.

## Arcade's default Google OAuth provider

Arcade provides a default Google OAuth provider (the Arcade-managed Google app) that you can use to quickly get started. This provider supports a fixed set of scopes and is shared across all Arcade users.

### Supported scopes

The default Arcade Google OAuth provider supports the following scopes:

- `https://www.googleapis.com/auth/calendar.readonly`
- `https://www.googleapis.com/auth/calendar.events`
- `https://www.googleapis.com/auth/calendar.events.readonly`
- `https://www.googleapis.com/auth/calendar.settings.readonly`
- `https://www.googleapis.com/auth/contacts`
- `https://www.googleapis.com/auth/contacts.readonly`
- `https://www.googleapis.com/auth/drive.file`
- `https://www.googleapis.com/auth/gmail.readonly`
- `https://www.googleapis.com/auth/gmail.compose`
- `https://www.googleapis.com/auth/gmail.send`
- `https://www.googleapis.com/auth/gmail.modify`
- `https://www.googleapis.com/auth/gmail.labels`
- `https://www.googleapis.com/auth/userinfo.email`
- `https://www.googleapis.com/auth/userinfo.profile`
- `openid`

<Callout type="warning">
If you try to use a scope that is not in this list with the default Arcade Google provider, you will get a `400 invalid authorization challenge: requesting unsupported scopes` error. For example, scopes like `https://www.googleapis.com/auth/drive.readonly` are not supported.

To use scopes beyond this list, you must create your own Google OAuth provider (see below).
</Callout>

### Limitations

The default provider has some limitations:

- **Fixed scope list**: You can only use the scopes listed above
- **Shared rate limits**: All Arcade users share the same rate limits
- **Arcade branding**: Your users will see "Arcade" as the requesting application

<Callout type="info">
For production use, we strongly recommend creating your own Google OAuth provider. This gives you:

- Full control over which scopes to request
- Dedicated rate limits for your application
- Your own branding in the OAuth consent screen
- Better security and compliance with your organization's policies
</Callout>

## Configuring your own Google OAuth provider

<Callout type="info">
When using your own app credentials, make sure you configure your project to
use a [custom user
Expand All @@ -40,14 +91,15 @@ Before showing how to configure your Google app credentials, let's go through th
- Follow Google's guide to [setting up OAuth credentials](https://support.google.com/cloud/answer/6158849?hl=en)
- Choose the [scopes](https://developers.google.com/identity/protocols/oauth2/scopes) (permissions) you need for your app
- At a minimum, you must enable these scopes:

- `https://www.googleapis.com/auth/userinfo.email`
- `https://www.googleapis.com/auth/userinfo.profile`
- Add the redirect URL generated by Arcade (see below) to the Authorized redirect URIs list
- Copy the client ID and client secret to use below

Next, add the Google app to Arcade.

## Configuring your own Google Auth Provider in Arcade
### Setting up your Google OAuth provider in Arcade


<Tabs items={["Dashboard GUI"]}>
Expand Down
8 changes: 4 additions & 4 deletions app/en/home/landing-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ export function LandingPage() {
<section className="relative isolate px-6 lg:px-8">
<div
aria-hidden="true"
className="-top-24 -z-10 sm:-top-40 absolute inset-x-0 transform-gpu overflow-hidden blur-3xl"
className="absolute inset-x-0 -top-24 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-40"
>
<div
className="-translate-x-1/2 relative left-[calc(50%-11rem)] aspect-[1155/678] w-[24rem] rotate-[30deg] bg-gradient-to-tr from-[#ee175e] to-[#9089fc] opacity-20 sm:left-[calc(50%-30rem)] sm:w-[48rem]"
className="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[24rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-[#ee175e] to-[#9089fc] opacity-20 sm:left-[calc(50%-30rem)] sm:w-[48rem]"
style={{
clipPath:
"polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
Expand Down Expand Up @@ -148,10 +148,10 @@ export function LandingPage() {
</div>
<div
aria-hidden="true"
className="-z-10 absolute inset-x-0 top-[calc(100%-13rem)] transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]"
className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]"
>
<div
className="-translate-x-1/2 relative left-[calc(50%+3rem)] aspect-[1155/678] w-[24rem] bg-gradient-to-tr from-[#ee175e] to-[#9089fc] opacity-20 sm:left-[calc(50%+36rem)] sm:w-[48rem]"
className="relative left-[calc(50%+3rem)] aspect-[1155/678] w-[24rem] -translate-x-1/2 bg-gradient-to-tr from-[#ee175e] to-[#9089fc] opacity-20 sm:left-[calc(50%+36rem)] sm:w-[48rem]"
style={{
clipPath:
"polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
Expand Down
4 changes: 2 additions & 2 deletions app/en/mcp-servers/components/filters-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function FiltersBar({ resultsCount }: FiltersBarProps) {
<div className="flex flex-wrap items-center justify-between gap-3">
{/* Search */}
<div className="relative w-full flex-1 sm:min-w-[200px] sm:max-w-md">
<Search className="-translate-y-1/2 absolute top-1/2 left-3 h-4 w-4 text-gray-500 dark:text-gray-400" />
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-500 dark:text-gray-400" />
<Input
className="pr-9 pl-9"
onChange={handleSearchChange}
Expand All @@ -65,7 +65,7 @@ export function FiltersBar({ resultsCount }: FiltersBarProps) {
{searchQuery && (
<button
aria-label="Clear search"
className="-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
className="absolute top-1/2 right-3 -translate-y-1/2 text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
onClick={() => setSearchQuery("")}
type="button"
>
Expand Down
Loading