Ir para o conteúdo
decodecodeveloper docs
Studio → MCP Apps e extensões → Criar MCP Apps

Criando MCP Apps

Crie interfaces interativas para tools MCP com o SDK de MCP Apps

O que é um MCP App?

Um MCP App é uma interface interativa exibida dentro de clientes MCP (Claude, Cursor etc.). Ele se conecta ao host pelo SDK @modelcontextprotocol/ext-apps, recebe entradas e resultados de tools e os apresenta em uma interface interativa.

MCP Apps são:

  • Compilados como bundles HTML de arquivo único
  • Servidos como resources MCP pelo seu servidor
  • Conectados às tools por _meta.ui.resourceUri
  • Estilizados automaticamente pelo tema do host

Stack

  • React 19 com React Compiler (babel-plugin-react-compiler)
  • Tailwind v4 + componentes shadcn/ui
  • TanStack Router (roteamento por hash, compatível com contextos incorporados)
  • SDK @modelcontextprotocol/ext-apps para comunicação com o host
  • Vite + vite-plugin-singlefile (compila para um único arquivo HTML)

Como funciona

  1. Seu servidor MCP expõe tools com _meta.ui.resourceUri apontando para um resource
  2. Quando uma tool é chamada, o cliente MCP carrega o resource (um bundle HTML)
  3. O bundle HTML se conecta ao host pelo SDK de MCP Apps
  4. A interface recebe as entradas e os resultados das tools por handlers de eventos
  5. A interface apresenta o resultado de forma interativa e pode enviar mensagens de volta à conversa

Estrutura do app

web/app.tsx — Ponto de entrada React

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { McpProvider } from "./context.tsx";
import { AppRouter } from "./router.tsx";
 
createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <McpProvider>
      <AppRouter />
    </McpProvider>
  </StrictMode>,
);

web/context.tsx — Gerenciamento de estado MCP

O McpProvider se conecta ao host MCP e gerencia o estado:

import {
  type App,
  type McpUiHostContext,
  useApp,
  useHostStyles,
} from "@modelcontextprotocol/ext-apps/react";
 
export function McpProvider({ children }: { children: ReactNode }) {
  const [state, setState] = useState<McpState>(INITIAL_STATE);
 
  const onAppCreated = useCallback((app: App) => {
    app.ontoolinput = (params) => {
      setState((prev) => ({
        ...prev,
        status: "tool-input",
        toolInput: params.arguments,
      }));
    };
 
    app.ontoolresult = (result) => {
      setState((prev) => ({
        ...prev,
        status: "tool-result",
        toolResult: result.structuredContent,
      }));
    };
 
    app.ontoolcancelled = () => {
      setState((prev) => ({ ...prev, status: "tool-cancelled" }));
    };
  }, []);
 
  const { app, isConnected } = useApp({
    appInfo: { name: "MCP App", version: "1.0.0" },
    capabilities: {},
    onAppCreated,
  });
 
  useHostStyles(app, app?.getHostContext());
 
  return (
    <McpAppContext.Provider value={app}>
      <McpStateContext.Provider value={state}>
        {children}
      </McpStateContext.Provider>
    </McpAppContext.Provider>
  );
}

Exports:

  • useMcpState<TInput, TResult>() — acesso tipado ao estado MCP atual
  • useMcpApp() — acesso à instância de App (para sendMessage() etc.)
  • useMcpHostContext() — acesso ao contexto do host (margens da área segura, informações da tool)

web/router.tsx — Roteamento de tools

const TOOL_PAGES: Record<string, React.ComponentType> = {
  hello_world: HelloPage,
};
 
function ToolRouter() {
  const { toolName } = useMcpState();
  const Page = TOOL_PAGES[toolName];
  if (!Page) return <p>Unknown tool: {toolName}</p>;
  return <Page />;
}

O router lê toolName do contexto do host MCP e renderiza a página correspondente. O RootLayout aplica as margens da área segura fornecidas pelo host.

web/types.ts — Tipos de estado

export type McpStatus =
  | "initializing"
  | "connected"
  | "tool-input"
  | "tool-result"
  | "tool-cancelled"
  | "error";
 
export interface McpState<TInput = unknown, TResult = unknown> {
  status: McpStatus;
  toolName?: string;
  error?: string;
  toolInput?: TInput;
  toolResult?: TResult;
}

Criando uma interface para uma tool

Passo a passo: adicione uma nova interface de tool

  1. Crie a tool (api/tools/my-tool.ts) com _meta.ui.resourceUri
  2. Crie o componente de interface (web/tools/my-tool/index.tsx)
  3. Registre no router — adicione a entrada em TOOL_PAGES no arquivo web/router.tsx
  4. Crie o resource (api/resources/my-tool.ts) que serve o bundle HTML
  5. Registre o resource em api/app.ts (array resources no template revisado)

Exemplo: componente de interface de uma tool

Esta é a interface da tool de saudação (web/tools/hello/index.tsx):

import { Badge } from "@/components/ui/badge.tsx";
import { Button } from "@/components/ui/button.tsx";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx";
import { useMcpApp, useMcpState } from "@/context.tsx";
import type { HelloInput, HelloOutput } from "../../../api/tools/hello.ts";
 
export default function HelloPage() {
  const state = useMcpState<HelloInput, HelloOutput>();
  const app = useMcpApp();
 
  if (state.status === "initializing") {
    return <div>Connecting to host...</div>;
  }
 
  if (state.status === "connected") {
    return (
      <Card>
        <CardHeader>
          <CardTitle>Hello MCP App</CardTitle>
        </CardHeader>
        <CardContent>
          <p>
            Connected. Call the <Badge variant="secondary">hello_world</Badge>{" "}
            tool to see a greeting here.
          </p>
        </CardContent>
      </Card>
    );
  }
 
  if (state.status === "tool-input") {
    return <div>Greeting {state.toolInput?.name ?? "someone"}...</div>;
  }
 
  if (state.status === "error") {
    return <p className="text-destructive">{state.error}</p>;
  }
 
  if (state.status === "tool-cancelled") {
    return <p className="text-destructive">Tool call was cancelled.</p>;
  }
 
  // tool-result
  return (
    <Card>
      <CardHeader>
        <CardTitle>{state.toolResult?.greeting}</CardTitle>
      </CardHeader>
      <CardContent>
        <Button
          onClick={() => {
            app?.sendMessage({
              role: "user",
              content: [{ type: "text", text: "Tell me more!" }],
            });
          }}
        >
          Send Message
        </Button>
      </CardContent>
    </Card>
  );
}

Padrões principais:

  • Use useMcpState<TInput, TResult>() com os tipos de entrada e saída da sua tool
  • Trate todos os estados (initializing, connected, tool-input, tool-result, tool-cancelled, error)
  • Use app.sendMessage() para enviar mensagens de volta à conversa

Exemplo: definição de resource

Veja Resources para criar o resource que serve esta interface.

Integração com o host

  • useHostStyles() — herda automaticamente o tema do host (cores, fontes, espaçamento)
  • useMcpHostContext() — acessa as margens da área segura e as informações da tool fornecidas pelo host
  • app.sendMessage() — envia mensagens de volta à conversa com a IA

Estilização

  • Componentes shadcn/ui disponíveis em web/components/ui/
  • Tailwind v4 para classes utilitárias
  • Estilos do host aplicados automaticamente por useHostStyles() — sua interface acompanha o tema do host
  • Projete para contextos incorporados: use as margens da área segura e layouts compactos