Skip to main content
This guide will walk you through the process of building an OAuth-compatible MCP client in Typescript. We’ll be using Next.js and the Model Context Protocol SDK. It builds off of the official example from the MCP TypeScript SDK.

1. Install dependencies

First, create a new Next.js project and install the MCP TypeScript SDK.
npx create-next-app@latest my-mcp-client
cd my-mcp-client
npm install @modelcontextprotocol/sdk

2. Create Library Files

You’ll want to create a /lib directory for common code across your application. This will contain the core logic for the MCP client and the authentication flow. Then, create the following files in your /lib directory:

Session Store

This file contains a simple in-memory session store. For production applications, you should use a more robust solution like Redis or a database.
/lib/session-store.ts
import { MCPOAuthClient } from "./oauth-client";

// Simple in-memory session store for demo purposes
// In production, use Redis, database, or proper session management
class SessionStore {
  private clients = new Map<string, MCPOAuthClient>();

  setClient(sessionId: string, client: MCPOAuthClient) {
    this.clients.set(sessionId, client);
  }

  getClient(sessionId: string): MCPOAuthClient | null {
    return this.clients.get(sessionId) || null;
  }

  removeClient(sessionId: string) {
    const client = this.clients.get(sessionId);
    if (client) {
      client.disconnect();
      this.clients.delete(sessionId);
    }
  }

  generateSessionId(): string {
    return Math.random().toString(36).substring(2) + Date.now().toString(36);
  }
}

export const sessionStore = new SessionStore();

OAuth Client

This file contains the core logic for the MCP OAuth client.
/lib/oauth-client.ts
import { URL } from "node:url";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import {
  OAuthClientInformation,
  OAuthClientInformationFull,
  OAuthClientMetadata,
  OAuthTokens,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import {
  CallToolRequest,
  ListToolsRequest,
  CallToolResultSchema,
  ListToolsResultSchema,
  ListToolsResult,
  CallToolResult,
} from "@modelcontextprotocol/sdk/types.js";
import {
  OAuthClientProvider,
  UnauthorizedError,
} from "@modelcontextprotocol/sdk/client/auth.js";

class InMemoryOAuthClientProvider implements OAuthClientProvider {
  private _clientInformation?: OAuthClientInformationFull;
  private _tokens?: OAuthTokens;
  private _codeVerifier?: string;

  constructor(
    private readonly _redirectUrl: string | URL,
    private readonly _clientMetadata: OAuthClientMetadata,
    onRedirect?: (url: URL) => void
  ) {
    this._onRedirect =
      onRedirect ||
      ((url) => {
        console.log(`Redirect to: ${url.toString()}`);
      });
  }

  private _onRedirect: (url: URL) => void;

  get redirectUrl(): string | URL {
    return this._redirectUrl;
  }

  get clientMetadata(): OAuthClientMetadata {
    return this._clientMetadata;
  }

  clientInformation(): OAuthClientInformation | undefined {
    return this._clientInformation;
  }

  saveClientInformation(clientInformation: OAuthClientInformationFull): void {
    this._clientInformation = clientInformation;
  }

  tokens(): OAuthTokens | undefined {
    return this._tokens;
  }

  saveTokens(tokens: OAuthTokens): void {
    this._tokens = tokens;
  }

  redirectToAuthorization(authorizationUrl: URL): void {
    this._onRedirect(authorizationUrl);
  }

  saveCodeVerifier(codeVerifier: string): void {
    this._codeVerifier = codeVerifier;
  }

  codeVerifier(): string {
    if (!this._codeVerifier) {
      throw new Error("No code verifier saved");
    }
    return this._codeVerifier;
  }
}

export class MCPOAuthClient {
  private client: Client | null = null;
  private oauthProvider: InMemoryOAuthClientProvider | null = null;

  constructor(
    private serverUrl: string,
    private callbackUrl: string,
    private onRedirect: (url: string) => void
  ) {}

  async connect(): Promise<void> {
    const clientMetadata: OAuthClientMetadata = {
      client_name: "Next.js MCP OAuth Client",
      redirect_uris: [this.callbackUrl],
      grant_types: ["authorization_code", "refresh_token"],
      response_types: ["code"],
      token_endpoint_auth_method: "client_secret_post",
      scope: "mcp:tools",
    };

    this.oauthProvider = new InMemoryOAuthClientProvider(
      this.callbackUrl,
      clientMetadata,
      (redirectUrl: URL) => {
        this.onRedirect(redirectUrl.toString());
      }
    );

    this.client = new Client(
      {
        name: "nextjs-oauth-client",
        version: "1.0.0",
      },
      { capabilities: {} }
    );

    await this.attemptConnection();
  }

  private async attemptConnection(): Promise<void> {
    if (!this.client || !this.oauthProvider) {
      throw new Error("Client not initialized");
    }

    const baseUrl = new URL(this.serverUrl);
    const transport = new StreamableHTTPClientTransport(baseUrl, {
      authProvider: this.oauthProvider,
    });

    try {
      await this.client.connect(transport);
    } catch (error) {
      if (error instanceof UnauthorizedError) {
        throw new Error("OAuth authorization required");
      } else {
        throw error;
      }
    }
  }

  async finishAuth(authCode: string): Promise<void> {
    if (!this.client || !this.oauthProvider) {
      throw new Error("Client not initialized");
    }

    const baseUrl = new URL(this.serverUrl);
    const transport = new StreamableHTTPClientTransport(baseUrl, {
      authProvider: this.oauthProvider,
    });

    await transport.finishAuth(authCode);
    await this.client.connect(transport);
  }

  async listTools(): Promise<ListToolsResult> {
    if (!this.client) {
      throw new Error("Not connected to server");
    }

    const request: ListToolsRequest = {
      method: "tools/list",
      params: {},
    };

    return await this.client.request(request, ListToolsResultSchema);
  }

  async callTool(
    toolName: string,
    toolArgs: Record<string, unknown>
  ): Promise<CallToolResult> {
    if (!this.client) {
      throw new Error("Not connected to server");
    }

    const request: CallToolRequest = {
      method: "tools/call",
      params: {
        name: toolName,
        arguments: toolArgs,
      },
    };

    return await this.client.request(request, CallToolResultSchema);
  }

  disconnect(): void {
    this.client = null;
    this.oauthProvider = null;
  }
}

3. Create OAuth API Routes

Next, you’ll want to create a few API routes to handle the OAuth flow. Create the following API routes in a directory at /app/api/mcp:
  • /app/api/mcp/auth/connect - Initiates the connection to the MCP server.
  • /app/api/mcp/auth/callback - Handles the OAuth callback from the MCP server.
  • /app/api/mcp/auth/finish - Finalizes the OAuth flow and stores the tokens.
  • /app/api/mcp/auth/disconnect - Disconnects from the MCP server.

Initialize the OAuth flow

This endpoint initiates the connection to the MCP server.
/app/api/mcp/auth/connect/route.ts
import { NextRequest, NextResponse } from "next/server";
import { MCPOAuthClient } from "@/lib/oauth-client";
import { sessionStore } from "@/lib/session-store";

interface ConnectRequestBody {
  serverUrl: string;
  callbackUrl: string;
}

export async function POST(request: NextRequest) {
  try {
    const body: ConnectRequestBody = await request.json();
    const { serverUrl, callbackUrl } = body;

    if (!serverUrl || !callbackUrl) {
      return NextResponse.json(
        { error: "Server URL and callback URL are required" },
        { status: 400 }
      );
    }

    const sessionId = sessionStore.generateSessionId();
    let authUrl: string | null = null;

    const client = new MCPOAuthClient(
      serverUrl,
      callbackUrl,
      (redirectUrl: string) => {
        authUrl = redirectUrl;
      }
    );

    try {
      await client.connect();
      // If we get here, connection succeeded without OAuth
      sessionStore.setClient(sessionId, client);
      return NextResponse.json({ success: true, sessionId });
    } catch (error: unknown) {
      if (error instanceof Error) {
        if (error.message === "OAuth authorization required" && authUrl) {
          // Store client for later use
          sessionStore.setClient(sessionId, client);
          return NextResponse.json(
            { requiresAuth: true, authUrl, sessionId },
            { status: 401 }
          );
        } else {
          return NextResponse.json(
            { error: error.message || "Unknown error" },
            { status: 500 }
          );
        }
      }
    }
  } catch (error: unknown) {
    if (error instanceof Error) {
      return NextResponse.json({ error: error.message }, { status: 500 });
    }
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
}

Handle the OAuth callback

This is the OAuth callback endpoint.
/app/api/mcp/auth/callback/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const code = searchParams.get("code");
  const error = searchParams.get("error");

  if (code) {
    const html = `
      <html>
        <body>
          <h1>Authorization Successful!</h1>
          <p>You can close this window and return to the app.</p>
          <script>
            // Send the auth code to the parent window
            if (window.opener) {
              window.opener.postMessage({ type: 'oauth-success', code: '${code}' }, '*');
              window.close();
            } else {
              // Fallback: redirect to main app with code
              window.location.href = '/?code=${code}';
            }
          </script>
        </body>
      </html>
    `;

    return new NextResponse(html, {
      headers: { "Content-Type": "text/html" },
    });
  } else if (error) {
    const html = `
      <html>
        <body>
          <h1>Authorization Failed</h1>
          <p>Error: ${error}</p>
          <script>
            if (window.opener) {
              window.opener.postMessage({ type: 'oauth-error', error: '${error}' }, '*');
              window.close();
            } else {
              // Fallback: redirect to main app with error
              window.location.href = '/?error=${error}';
            }
          </script>
        </body>
      </html>
    `;

    return new NextResponse(html, {
      headers: { "Content-Type": "text/html" },
    });
  }

  return new NextResponse("Bad request", { status: 400 });
}

Finalize the OAuth flow

This endpoint finalizes the OAuth flow.
/app/api/mcp/auth/finish/route.ts
import { NextRequest, NextResponse } from "next/server";
import { sessionStore } from "@/lib/session-store";

interface FinishAuthRequestBody {
  authCode: string;
  sessionId: string;
}

export async function POST(request: NextRequest) {
  try {
    const body: FinishAuthRequestBody = await request.json();
    const { authCode, sessionId } = body;

    if (!authCode || !sessionId) {
      return NextResponse.json(
        { error: "Authorization code and session ID are required" },
        { status: 400 }
      );
    }

    const client = sessionStore.getClient(sessionId);

    if (!client) {
      return NextResponse.json(
        { error: "No active OAuth session found" },
        { status: 400 }
      );
    }

    await client.finishAuth(authCode);

    return NextResponse.json({ success: true });
  } catch (error: unknown) {
    if (error instanceof Error) {
      return NextResponse.json({ error: error.message }, { status: 500 });
    }
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
}

Disconnect from the MCP server

This endpoint disconnects from the MCP server.
/app/api/mcp/auth/disconnect/route.ts
import { NextRequest, NextResponse } from "next/server";
import { sessionStore } from "@/lib/session-store";

interface DisconnectRequestBody {
  sessionId: string;
}

export async function POST(request: NextRequest) {
  try {
    const body: DisconnectRequestBody = await request.json();
    const { sessionId } = body;

    if (!sessionId) {
      return NextResponse.json(
        { error: "Session ID is required" },
        { status: 400 }
      );
    }

    sessionStore.removeClient(sessionId);

    return NextResponse.json({ success: true });
  } catch (error: unknown) {
    if (error instanceof Error) {
      return NextResponse.json({ error: error.message }, { status: 500 });
    }
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
}

4. List/Call Tools

Next, you’ll want to create a few API routes to handle actually using the tools provided by the MCP server. Create the following API routes in a directory at /app/api/mcp:
  • /app/api/mcp/tool/list - Lists the available tools on the MCP server.
  • /app/api/mcp/tool/call - Calls a tool on the MCP server.

List the available tools

This endpoint lists the available tools on the MCP server.
/app/api/mcp/tool/list/route.ts
import { NextRequest, NextResponse } from "next/server";
import { sessionStore } from "@/lib/session-store";

export async function GET(request: NextRequest) {
  try {
    const sessionId = request.nextUrl.searchParams.get("sessionId");

    if (!sessionId) {
      return NextResponse.json(
        { error: "Session ID is required" },
        { status: 400 }
      );
    }

    const client = sessionStore.getClient(sessionId);

    if (!client) {
      return NextResponse.json(
        { error: "Not connected to server" },
        { status: 400 }
      );
    }

    const result = await client.listTools();

    return NextResponse.json({ tools: result.tools || [] });
  } catch (error: unknown) {
    if (error instanceof Error) {
      return NextResponse.json({ error: error.message }, { status: 500 });
    }
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
}

Call a tool

This endpoint calls a tool on the MCP server.
/app/api/mcp/tool/call/route.ts
import { NextRequest, NextResponse } from "next/server";
import { sessionStore } from "@/lib/session-store";

interface CallToolRequestBody {
  toolName: string;
  toolArgs?: Record<string, unknown>;
  sessionId: string;
}

export async function POST(request: NextRequest) {
  try {
    const body: CallToolRequestBody = await request.json();
    const { toolName, toolArgs, sessionId } = body;

    if (!toolName || !sessionId) {
      return NextResponse.json(
        { error: "Tool name and session ID are required" },
        { status: 400 }
      );
    }

    const client = sessionStore.getClient(sessionId);

    if (!client) {
      return NextResponse.json(
        { error: "Not connected to server" },
        { status: 400 }
      );
    }

    const result = await client.callTool(toolName, toolArgs || {});

    return NextResponse.json({ result });
  } catch (error: unknown) {
    if (error instanceof Error) {
      return NextResponse.json({ error: error.message }, { status: 500 });
    }
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
}

5. Build the UI

Now, you’ll want to build the UI to call these endpoints. You can use the following code to get started:
/app/page.tsx
"use client";

import { useState } from "react";

interface SchemaProperty {
  type?: string;
  description?: string;
  default?: unknown;
}

interface Tool {
  name: string;
  description?: string;
  inputSchema?: {
    type: "object";
    properties?: Record<string, SchemaProperty>;
    required?: string[];
  };
}

export default function Home() {
  const [serverUrl, setServerUrl] = useState(
    "https://server.smithery.ai/exa/mcp"
  );
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [tools, setTools] = useState<Tool[]>([]);
  const [selectedTool, setSelectedTool] = useState("");
  const [toolArgs, setToolArgs] = useState("{}");
  const [toolResult, setToolResult] = useState<object | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [jsonError, setJsonError] = useState<string | null>(null);

  const generateDefaultArgs = (tool: Tool): string => {
    if (!tool.inputSchema?.properties) {
      return "{}";
    }

    const defaultArgs: Record<string, unknown> = {};

    Object.entries(tool.inputSchema.properties).forEach(([key, schema]) => {
      switch (schema.type) {
        case "string":
          defaultArgs[key] = schema.default || "";
          break;
        case "number":
        case "integer":
          defaultArgs[key] = schema.default || 0;
          break;
        case "boolean":
          defaultArgs[key] = schema.default || false;
          break;
        case "array":
          defaultArgs[key] = schema.default || [];
          break;
        case "object":
          defaultArgs[key] = schema.default || {};
          break;
        default:
          defaultArgs[key] = schema.default || null;
      }
    });

    return JSON.stringify(defaultArgs, null, 2);
  };

  const handleToolSelect = (toolName: string) => {
    setSelectedTool(toolName);
    setJsonError(null);

    if (toolName) {
      const tool = tools.find((t) => t.name === toolName);
      if (tool) {
        setToolArgs(generateDefaultArgs(tool));
      }
    } else {
      setToolArgs("{}");
    }
  };

  const handleArgsChange = (value: string) => {
    setToolArgs(value);

    // Validate JSON syntax
    try {
      if (value.trim()) {
        JSON.parse(value);
      }
      setJsonError(null);
    } catch (e) {
      setJsonError(e instanceof Error ? e.message : "Invalid JSON");
    }
  };

  const getCallbackUrl = () => {
    return `${window.location.origin}/api/mcp/auth/callback`;
  };

  const handleConnect = async () => {
    if (!serverUrl) return;

    setLoading(true);
    setError(null);
    setJsonError(null);

    try {
      const response = await fetch("/api/mcp/auth/connect", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ serverUrl, callbackUrl: getCallbackUrl() }),
      });

      const data = await response.json();

      if (!response.ok) {
        if (data.requiresAuth && data.authUrl && data.sessionId) {
          setSessionId(data.sessionId);
          // Open authorization URL in a popup
          const popup = window.open(
            data.authUrl,
            "oauth-popup",
            "width=600,height=700,scrollbars=yes,resizable=yes"
          );

          // Listen for messages from the popup
          const messageHandler = async (event: MessageEvent) => {
            if (event.origin !== window.location.origin) return;

            if (event.data.type === "oauth-success") {
              popup?.close();

              try {
                const finishResponse = await fetch("/api/mcp/auth/finish", {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: JSON.stringify({
                    authCode: event.data.code,
                    sessionId: data.sessionId,
                  }),
                });

                if (finishResponse.ok) {
                  setIsConnected(true);
                  await loadTools(data.sessionId);
                } else {
                  const errorData = await finishResponse.json();
                  setError(
                    `Failed to complete authentication: ${errorData.error}`
                  );
                }
              } catch (err) {
                setError(`Failed to complete authentication: ${err}`);
              }

              window.removeEventListener("message", messageHandler);
            } else if (event.data.type === "oauth-error") {
              popup?.close();
              setError(`OAuth failed: ${event.data.error}`);
              window.removeEventListener("message", messageHandler);
            }
          };

          window.addEventListener("message", messageHandler);
        } else {
          setError(data.error || "Connection failed");
        }
      } else {
        setSessionId(data.sessionId);
        setIsConnected(true);
        await loadTools(data.sessionId);
      }
    } catch (err: unknown) {
      if (err instanceof Error) {
        setError(`Connection failed: ${err.message}`);
      } else {
        setError(`Connection failed: ${err}`);
      }
    } finally {
      setLoading(false);
    }
  };

  const loadTools = async (currentSessionId?: string) => {
    const sid = currentSessionId || sessionId;
    if (!sid) return;

    try {
      const response = await fetch(`/api/mcp/tool/list?sessionId=${sid}`);
      const data = await response.json();

      if (response.ok) {
        setTools(data.tools || []);
      } else {
        setError(`Failed to load tools: ${data.error}`);
      }
    } catch (err) {
      setError(`Failed to load tools: ${err}`);
    }
  };

  const handleCallTool = async () => {
    if (!selectedTool) return;

    setLoading(true);
    setError(null);
    setToolResult(null);

    try {
      let args = {};
      if (toolArgs.trim()) {
        args = JSON.parse(toolArgs);
      }

      const response = await fetch("/api/mcp/tool/call", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          toolName: selectedTool,
          toolArgs: args,
          sessionId,
        }),
      });

      const data = await response.json();

      if (response.ok) {
        setToolResult(data.result);
      } else {
        setError(`Tool call failed: ${data.error}`);
      }
    } catch (err: unknown) {
      if (err instanceof Error) {
        setError(`Tool call failed: ${err.message}`);
      } else {
        setError(`Tool call failed: ${err}`);
      }
    } finally {
      setLoading(false);
    }
  };

  const handleDisconnect = async () => {
    try {
      if (sessionId) {
        await fetch("/api/mcp/auth/disconnect", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ sessionId }),
        });
      }
    } catch {
      // Ignore disconnect errors
    }

    setSessionId(null);
    setIsConnected(false);
    setTools([]);
    setSelectedTool("");
    setToolResult(null);
    setJsonError(null);
  };

  return (
    <div className="font-sans min-h-screen p-8 max-w-4xl mx-auto">
      <h1 className="text-3xl font-bold mb-8">MCP OAuth Client</h1>

      {error && (
        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
          {error}
        </div>
      )}

      {!isConnected ? (
        <div className="space-y-4">
          <div>
            <label
              htmlFor="serverUrl"
              className="block text-sm font-medium mb-2"
            >
              MCP Server URL:
            </label>
            <input
              id="serverUrl"
              type="text"
              value={serverUrl}
              onChange={(e) => setServerUrl(e.target.value)}
              placeholder="http://localhost:3000/mcp"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
          </div>

          <button
            onClick={handleConnect}
            disabled={!serverUrl || loading}
            className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {loading ? "Connecting..." : "Connect"}
          </button>
        </div>
      ) : (
        <div className="space-y-6">
          <div className="flex items-center justify-between">
            <div className="text-green-600 font-medium">
Connected to {serverUrl}
            </div>
            <button
              onClick={handleDisconnect}
              className="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
            >
              Disconnect
            </button>
          </div>

          <div>
            <h2 className="text-xl font-semibold mb-4">Call Tool</h2>
            <div className="space-y-4">
              <div>
                <label
                  htmlFor="toolSelect"
                  className="block text-sm font-medium mb-2"
                >
                  Select Tool:
                </label>
                <select
                  id="toolSelect"
                  value={selectedTool}
                  onChange={(e) => handleToolSelect(e.target.value)}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                >
                  <option value="">Select a tool...</option>
                  {tools.map((tool, index) => (
                    <option key={index} value={tool.name}>
                      {tool.name}
                    </option>
                  ))}
                </select>
              </div>

              {selectedTool &&
                (() => {
                  const tool = tools.find((t) => t.name === selectedTool);
                  return tool?.inputSchema?.properties ? (
                    <div className="bg-blue-50 p-3 rounded-md">
                      <h4 className="text-sm font-medium text-blue-800 mb-2">
                        Expected Parameters:
                      </h4>
                      <div className="space-y-1">
                        {Object.entries(tool.inputSchema.properties).map(
                          ([key, schema]) => (
                            <div key={key} className="text-sm">
                              <span className="font-medium text-blue-700">
                                {key}
                              </span>
                              {tool.inputSchema?.required?.includes(key) && (
                                <span className="text-red-500 ml-1">*</span>
                              )}
                              <span className="text-gray-600 ml-2">
                                ({schema.type || "any"})
                              </span>
                              {schema.description && (
                                <div className="text-gray-500 ml-4 text-xs">
                                  {schema.description}
                                </div>
                              )}
                            </div>
                          )
                        )}
                      </div>
                    </div>
                  ) : null;
                })()}

              <div>
                <label
                  htmlFor="toolArgs"
                  className="block text-sm font-medium mb-2"
                >
                  Arguments (JSON):
                </label>
                <textarea
                  id="toolArgs"
                  value={toolArgs}
                  onChange={(e) => handleArgsChange(e.target.value)}
                  placeholder="{}"
                  rows={3}
                  className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 ${
                    jsonError
                      ? "border-red-300 focus:ring-red-500"
                      : "border-gray-300 focus:ring-blue-500"
                  }`}
                />
                {jsonError && (
                  <div className="mt-1 text-sm text-red-600">{jsonError}</div>
                )}
              </div>

              <button
                onClick={handleCallTool}
                disabled={!selectedTool || loading || !!jsonError}
                className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed"
              >
                {loading ? "Calling..." : "Call Tool"}
              </button>
            </div>
          </div>

          {toolResult && (
            <div>
              <h2 className="text-xl font-semibold mb-4">Tool Result</h2>
              <div className="bg-gray-100 p-4 rounded-md">
                <pre className="whitespace-pre-wrap text-sm">
                  {JSON.stringify(toolResult, null, 2)}
                </pre>
              </div>
            </div>
          )}

          <div>
            <h2 className="text-xl font-semibold mb-4">Available Tools</h2>
            {tools.length > 0 ? (
              <div className="space-y-2">
                {tools.map((tool, index) => (
                  <div
                    key={index}
                    className="p-3 border border-gray-200 rounded-md"
                  >
                    <div className="font-medium">{tool.name}</div>
                    {tool.description && (
                      <div className="text-sm text-gray-600 mt-1">
                        {tool.description}
                      </div>
                    )}
                  </div>
                ))}
              </div>
            ) : (
              <div className="text-gray-500">No tools available</div>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

Putting it all together

With these files in place, your Next.js application can now interact with an MCP server.

Authentication Endpoints

  • Connect: POST /api/mcp/auth/connect with serverUrl and callbackUrl in the body. The callbackUrl should point to /api/mcp/auth/callback.
  • List Tools: GET /api/mcp/tool/list?sessionId=<sessionId>
  • Call Tool: POST /api/mcp/tool/call with toolName, toolArgs, and sessionId in the body.
  • Disconnect: POST /api/mcp/auth/disconnect with sessionId in the body.

Tool Endpoints

  • List Tools: GET /api/mcp/tool/list?sessionId=<sessionId>
  • Call Tool: POST /api/mcp/tool/call with toolName, toolArgs, and sessionId in the body.
Here’s what your directory structure should look like:
mcp-oauth-client/
  |-- next-env.d.ts
  |-- next.config.ts
  |-- package-lock.json
  |-- package.json
  |-- README.md
  |-- src/
  |   |-- app/
  |   |   |-- api/
  |   |   |   |-- mcp/
  |   |   |   |   |-- auth/
  |   |   |   |   |   |-- callback/
  |   |   |   |   |   |   |-- route.ts
  |   |   |   |   |   |-- connect/
  |   |   |   |   |   |   |-- route.ts
  |   |   |   |   |   |-- disconnect/
  |   |   |   |   |   |   |-- route.ts
  |   |   |   |   |   |-- finish/
  |   |   |   |   |       |-- route.ts
  |   |   |   |   |-- tool/
  |   |   |   |   |   |-- call/
  |   |   |   |   |   |   |-- route.ts
  |   |   |   |   |   |-- list/
  |   |   |   |   |       |-- route.ts
  |   |   |   |-- page.tsx
  |   |-- lib/
  |       |-- oauth-client.ts
  |       |-- session-store.ts
  |-- tsconfig.json
I