Integrate
Artificial intelligence

Anthropic Claude

Use MCP tools with Anthropic's Claude API

The Integrate SDK provides seamless integration with Anthropic's Claude API, allowing Claude to access your integrations through MCP tools.

Installation

Install the Integrate SDK and Anthropic SDK packages:

bun add integrate-sdk @anthropic-ai/sdk

Setup

Setting up Anthropic integration requires 3 files (or 5 files if not using a database):

1. Server Configuration

Create a server configuration file with your OAuth credentials:

// lib/integrate.ts
import { createMCPServer, githubIntegration } from "integrate-sdk/server";

export const { client: serverClient } = createMCPServer({
  apiKey: process.env.INTEGRATE_API_KEY,
  integrations: [
    githubIntegration({
      scopes: ["repo", "user"],
    }),
  ],
});

2. Providers (only if no database)

If you're not using a database, create a providers component that injects tokens:

// app/providers.tsx
"use client";

import { client } from "integrate-sdk";
import { useIntegrateAI } from "integrate-sdk/react";

export function Providers({ children }: { children: React.ReactNode }) {
  useIntegrateAI(client);
  return <>{children}</>;
}

3. Layout (only if no database)

Wrap your app with the Providers component:

// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

4. OAuth Handler

Create a catch-all route that handles OAuth operations at app/api/integrate/[...all]/route.ts:

import { serverClient } from "@/lib/integrate";
import { toNextJsHandler } from "integrate-sdk/server";

export const { POST, GET } = toNextJsHandler(serverClient);

5. Chat API Route

Create a chat route that converts MCP tools to Anthropic format:

// app/api/chat/route.ts
import { serverClient } from "@/lib/integrate";
import { getAnthropicTools } from "integrate-sdk/server";
import Anthropic from "@anthropic-ai/sdk";
import { handleAnthropicMessage } from "integrate-sdk/ai/anthropic";

const anthropic = new Anthropic();

export async function POST(req: Request) {
  const { messages } = await req.json();

  const msg = await anthropic.messages.create({
    model: "claude-sonnet-4-5",
    messages,
    tools: await getAnthropicTools(serverClient),
    max_tokens: 1000,
  });

  const result = await handleAnthropicMessage(serverClient, msg);

  return Response.json(result);
}

Usage

Use Anthropic's Claude in any component:

// app/page.tsx
"use client";

import { useState, useEffect } from "react";
import { client } from "integrate-sdk";
import Anthropic from "@anthropic-ai/sdk";
import { Streamdown } from "streamdown";

export default function ChatPage() {
  const [messages, setMessages] = useState<Anthropic.MessageParam[]>([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const [githubAuthorized, setGithubAuthorized] = useState(false);

  useEffect(() => {
    client.isAuthorized("github").then(setGithubAuthorized);
  }, []);

  async function handleGithubClick() {
    try {
      if (githubAuthorized) {
        await client.disconnectProvider("github");
      } else {
        await client.authorize("github");
      }
      setGithubAuthorized(client.isAuthorized("github"));
    } catch (error) {
      console.error("Error:", error);
    }
  }

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!input.trim()) return;

    const userMessage: Anthropic.MessageParam = {
      role: "user",
      content: input,
    };
    setMessages((prev) => [...prev, userMessage]);
    setInput("");
    setLoading(true);

    try {
      const response = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          messages: [...messages, userMessage],
        }),
      });

      const data = await response.json();

      const newMessages = Array.isArray(data) ? data : [data];

      setMessages((prev) => [
        ...prev,
        ...newMessages.map((msg: Anthropic.MessageParam) => ({
          role: msg.role,
          content: msg.content,
        })),
      ]);
    } catch (error) {
      console.error("Error:", error);
    } finally {
      setLoading(false);
    }
  }

  const renderContent = (
    content: Anthropic.MessageParam["content"],
    isAssistant: boolean
  ) => {
    if (typeof content === "string") {
      if (isAssistant) {
        return <Streamdown isAnimating={loading}>{content}</Streamdown>;
      }
      return <div className="whitespace-pre-wrap">{content}</div>;
    }

    if (Array.isArray(content)) {
      return (
        <div className="space-y-2">
          {content.map((block, index) => {
            if (block.type === "text") {
              if (isAssistant) {
                return (
                  <Streamdown key={index} isAnimating={loading}>
                    {block.text}
                  </Streamdown>
                );
              }
              return (
                <div key={index} className="whitespace-pre-wrap">
                  {block.text}
                </div>
              );
            }
            if (block.type === "tool_use") {
              return null;
            }
            if (block.type === "tool_result") {
              let displayContent = block.content;

              if (typeof block.content === "string") {
                try {
                  const parsed = JSON.parse(block.content);
                  if (parsed.content && Array.isArray(parsed.content)) {
                    return (
                      <div key={index} className="space-y-2">
                        {parsed.content.map(
                          (
                            innerBlock: { type: string; text: string },
                            innerIndex: number
                          ) => {
                            if (innerBlock.type === "text") {
                              return (
                                <div
                                  key={innerIndex}
                                  className="whitespace-pre-wrap"
                                >
                                  {innerBlock.text}
                                </div>
                              );
                            }
                            return null;
                          }
                        )}
                      </div>
                    );
                  }
                  displayContent = JSON.stringify(parsed, null, 2);
                } catch {}
              }

              return (
                <div key={index} className="whitespace-pre-wrap">
                  {typeof displayContent === "string"
                    ? displayContent
                    : JSON.stringify(displayContent)}
                </div>
              );
            }
            return null;
          })}
        </div>
      );
    }

    return JSON.stringify(content);
  };

  return (
    <div className="flex flex-col h-screen">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((m, i) => {
          let hasRenderableContent = false;
          let isToolResult = false;

          if (typeof m.content === "string") hasRenderableContent = true;
          else if (Array.isArray(m.content)) {
            hasRenderableContent = m.content.some(
              (block) => block.type === "text" || block.type === "tool_result"
            );
            isToolResult = m.content.some(
              (block) => block.type === "tool_result"
            );
          }

          if (!hasRenderableContent) return null;

          const alignmentClass =
            m.role === "user" && !isToolResult ? "text-right" : "text-left";

          return (
            <div key={i} className={alignmentClass}>
              <div className="inline-block max-w-2xl">
                {renderContent(
                  m.content,
                  m.role === "assistant" || isToolResult
                )}
              </div>
            </div>
          );
        })}
      </div>
      <form
        onSubmit={handleSubmit}
        className="p-4 border-t border-gray-500 space-y-2"
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Ask Claude anything..."
          className="w-full px-4 py-2 border border-gray-500 rounded-lg"
          disabled={loading}
        />
        <div className="flex justify-between">
          <button
            type="button"
            onClick={handleGithubClick}
            className="px-4 py-2 bg-black text-white rounded-lg disabled:opacity-50"
          >
            {githubAuthorized ? "Disconnect" : "Connect"} GitHub
          </button>
          <button
            type="submit"
            disabled={loading || !input.trim()}
            className="px-4 py-2 bg-black text-white rounded-lg disabled:opacity-50"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  );
}

Next Steps