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
- Create an Arcade account
- 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
- 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
- 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;
}
}
}