HomeMCPArcade as an MCP Server

Connect to Arcade’s Remote MCP Server

In this guide, you’ll learn how to create a TypeScript client that can connect to Arcade’s remote MCP server.

Prerequisites

  1. Create an Arcade account
  2. Get an Arcade API key and take note, you’ll need it in the next steps.

Install Dependencies

npm install @modelcontextprotocol/sdk express open
npm install -D typescript tsx

Create your Public OAuth Client

  1. Create a new file called PublicOAuthClient.ts
import {
    OAuthClientProvider
} from '@modelcontextprotocol/sdk/client/auth.js';
 
import open from 'open';
 
import type {
    OAuthClientMetadata,
    OAuthClientInformation,
    OAuthTokens,
    OAuthClientInformationFull
} from '@modelcontextprotocol/sdk/shared/auth.js';
 
// Create a simple localStorage polyfill for Node.js environment
class NodeStorage implements Storage {
    private data: Record<string, string> = {};
 
    get length(): number {
        return Object.keys(this.data).length;
    }
 
    clear(): void {
        this.data = {};
    }
 
    getItem(key: string): string | null {
        return key in this.data ? this.data[key] : null;
    }
 
    key(index: number): string | null {
        return Object.keys(this.data)[index] || null;
    }
 
    removeItem(key: string): void {
        delete this.data[key];
    }
 
    setItem(key: string, value: string): void {
        this.data[key] = value;
    }
}
 
// Determine if we're in a browser or Node.js environment
const isNodeEnv = typeof window === 'undefined' || typeof localStorage === 'undefined';
const storageImplementation = isNodeEnv ? new NodeStorage() : localStorage;
 
/**
 * An implementation of OAuthClientProvider that works with standard OAuth 2.0 servers.
 * This implementation uses localStorage for persisting tokens and client information.
 */
export class PublicOAuthClient implements OAuthClientProvider {
    private storage: Storage;
    private readonly clientMetadataValue: OAuthClientMetadata;
    private readonly redirectUrlValue: string | URL;
    private readonly storageKeyPrefix: string;
    private readonly clientId: string;
 
    /**
     * Creates a new PublicOAuthClient
     *
     * @param client_id The OAuth client ID
     * @param clientMetadata The OAuth client metadata
     * @param redirectUrl The URL to redirect to after authorization
     * @param storageKeyPrefix Prefix for localStorage keys (default: 'mcp_oauth_')
     * @param storage Storage implementation (default: storageImplementation)
     */
    constructor(
        clientMetadata: OAuthClientMetadata,
        client_id: string,
        redirectUrl: string | URL,
        storageKeyPrefix = 'mcp_oauth_',
        storage = storageImplementation
    ) {
        this.clientId = client_id;
        this.clientMetadataValue = clientMetadata;
        this.redirectUrlValue = redirectUrl;
        this.storageKeyPrefix = storageKeyPrefix;
        this.storage = storage;
    }
 
    /**
     * The URL to redirect the user agent to after authorization.
     */
    get redirectUrl(): string | URL {
        return this.redirectUrlValue;
    }
 
    /**
     * Metadata about this OAuth client.
     */
    get clientMetadata(): OAuthClientMetadata {
        return this.clientMetadataValue;
    }
 
    /**
     * Loads information about this OAuth client from storage
     */
    clientInformation(): OAuthClientInformation | undefined {
        const clientInfoStr = this.storage.getItem(`${this.storageKeyPrefix}client_info`);
        if (!clientInfoStr) {
            // Return basic client information with client_id if nothing in storage
            return {
                client_id: this.clientId
            };
        }
 
        try {
            return JSON.parse(clientInfoStr) as OAuthClientInformation;
        } catch (e) {
            console.error('Failed to parse client information', e);
            return undefined;
        }
    }
 
    /**
     * Saves client information to storage
     */
    saveClientInformation(clientInformation: OAuthClientInformationFull): void {
        this.storage.setItem(
            `${this.storageKeyPrefix}client_info`,
            JSON.stringify(clientInformation)
        );
    }
 
    /**
     * Loads any existing OAuth tokens for the current session
     */
    tokens(): OAuthTokens | undefined {
        const tokensStr = this.storage.getItem(`${this.storageKeyPrefix}tokens`);
        if (!tokensStr) {
            return undefined;
        }
 
        try {
            return JSON.parse(tokensStr) as OAuthTokens;
        } catch (e) {
            console.error('Failed to parse tokens', e);
            return undefined;
        }
    }
 
    /**
     * Stores new OAuth tokens for the current session
     */
    saveTokens(tokens: OAuthTokens): void {
        this.storage.setItem(
            `${this.storageKeyPrefix}tokens`,
            JSON.stringify(tokens)
        );
    }
 
    /**
     * Redirects the user agent to the given authorization URL
     */
    redirectToAuthorization(authorizationUrl: URL): void {
        // TODO: Update MCP TS SDK to add state
        // TODO: Verify state in callback
        const state = crypto.randomUUID();
        authorizationUrl.searchParams.set('state', state);
 
        if (typeof window !== 'undefined') {
            window.location.href = authorizationUrl.toString();
        } else {
            console.log(`Opening URL: ${authorizationUrl.toString()}`);
            open(authorizationUrl.toString());
        }
    }
 
    /**
     * Saves a PKCE code verifier for the current session
     */
    saveCodeVerifier(codeVerifier: string): void {
        console.log("hit saveCodeVerifier");
        this.storage.setItem(`${this.storageKeyPrefix}code_verifier`, codeVerifier);
    }
 
    /**
     * Loads the PKCE code verifier for the current session
     */
    codeVerifier(): string {
        console.log("hit codeVerifier");
        const verifier = this.storage.getItem(`${this.storageKeyPrefix}code_verifier`);
        if (!verifier) {
            throw new Error('No code verifier found in storage');
        }
        return verifier;
    }
 
    /**
     * Clears all OAuth-related data from storage
     */
    clearAuth(): void {
        this.storage.removeItem(`${this.storageKeyPrefix}tokens`);
        this.storage.removeItem(`${this.storageKeyPrefix}code_verifier`);
    }
}

Create your MCP Client with OAuth support

  1. Create a new file called index.ts
 
import express from 'express';
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
import { PublicOAuthClient } from "./src/PublicOAuthClient.js";
import { OAuthClientMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
 
async function main() {
    // Start an Express server to handle the OAuth callback
    const app = express();
    const callbackServer = app.listen(3000);
 
    // Create a promise to resolve when we get the authorization code
    const codePromise = new Promise<string>((resolve) => {
        app.get('/callback', (req, res) => {
            const code = req.query.code as string;
            res.send('Authentication successful! You can close this window.');
 
            // Resolve the promise with the authorization code
            resolve(code);
        });
    });
 
    // Set up our MCP client with OAuth support
    const serverUrl = "https://api.arcade.dev/v1/mcps/beta/mcp";
 
    const clientMetadata: OAuthClientMetadata = {
        client_name: "My MCP Client",
        redirect_uris: ["http://localhost:3000/callback"],
    };
 
    const authProvider = new PublicOAuthClient(
        clientMetadata,
        "mcp_beta",
        "http://localhost:3000/callback"
    );
 
    let transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
        authProvider,
        requestInit: {
            headers: { Accept: "application/json" }
        }
    });
 
    const client = new Client({
        name: "example-client",
        version: "1.0.0",
    });
    console.log("Connecting to MCP...");
 
    try {
        // This will likely fail with UnauthorizedError
        try {
            await client.connect(transport);
            console.log("Connected without auth (unusual)");
        } catch (error: any) {
            if (error instanceof UnauthorizedError) {
                console.log("Authentication required, waiting for callback...");
                // Wait for the authorization code from the callback
                const code = await codePromise;
 
                // Complete the authentication with the code
                await transport.finishAuth(code);
                console.log("Auth complete");
 
                // Need to rebuild the transport (to reset it), but the authProvider is persistent
                transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
                    authProvider,
                    requestInit: {
                        headers: { Accept: "application/json" }
                    }
                });
 
                // Now try connecting again
                console.log("Connecting to MCP...");
                await client.connect(transport);
                console.log("Connected to MCP");
            } else {
                throw error;
            }
        }
 
        // List available tools
        console.log("Listing tools");
        const toolsResult = await client.listTools();
 
        console.log(`Available tools (${toolsResult.tools.length} tools):`);
        for (const tool of toolsResult.tools) {
            const firstLineOfDescription = tool.description?.split("\n")[0];
            console.log(`  - ${tool.name} (${firstLineOfDescription})`);
        }
 
        // Call a tool
        console.log("Calling tool math_multiply");
        await callTool(client, "math_multiply", {
            a: "2",
            b: "3",
        });
 
        // Call another tool
        console.log("Calling tool google_listemails");
        await callTool(client, "google_listemails", {
            n_emails: 3
        });
 
        console.log("Done! Goodbye");
    } catch (error) {
        console.error("Error:", error);
    } finally {
        await client.close();
        callbackServer.close();
        process.exit(0);
    }
}
 
main().catch(error => {
    console.error("Unhandled error:", error);
    process.exit(1);
});
 
async function callTool(client: Client, toolName: string, args: any) {
    try {
        const result = await client.callTool({
            name: toolName,
            arguments: args,
        });
 
        console.log("Tool result:");
        result.content.forEach((item) => {
            if (item.type === "text") {
                console.log(`  ${item.text}`);
            } else {
                console.log(`  ${item.type} content:`, item);
            }
        });
    } catch (error: any) {
        console.log("Error:", error);
        // Check if this is an interaction_required error
        if (error.code === -32003 && error.data && error.data.type === "url") {
            console.log("\n------------------------------------------");
            console.log(error.data.message.text);
            console.log(error.data.url);
            console.log("------------------------------------------\n");
 
            // Prompt user to press any key after authorization
            console.log(
                "After completing the authorization flow, press Enter to continue...",
            );
            await new Promise((resolve) => {
                process.stdin.once("data", () => {
                    resolve(undefined);
                });
                process.stdin.resume();
            });
 
            // Retry the tool call
            console.log("Retrying tool call after authorization...");
            await callTool(client, toolName, args);
        } else {
            // Re-throw other errors
            throw error;
        }
    }
}