Skip to content

MCP Apps example

The examples/ui/mcp project connects a Python MCP tool to a React widget. It demonstrates the complete path from a tool result to a host-connected widget.

Demonstrates

  • BelgieExtension registration with a Path to widget.tsx.
  • Vite configuration with @belgie/mcp/vite.
  • Generated typed callers and useToolResult.
  • A self-contained production widget build.

Run the example

Install the example's dependencies and start its server:

cd examples/ui/mcp
uv sync
uv run main

The example listens on http://127.0.0.1:3001. Its MCP streamable HTTP endpoint is /mcp.

Before starting the server, lock and install the JavaScript dependencies if the checkout does not already contain a current deno.lock:

uv run belgie lock
uv run belgie install

Python server

The server registers a normal Python function with a widget path. The complete entrypoint is included from the shipped example:

from datetime import UTC, datetime
from pathlib import Path
from typing import Final, TypedDict

import uvicorn
from mcp.server import MCPServer

from belgie.mcp import BelgieExtension

PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
WIDGET: Final[Path] = PROJECT_ROOT / "src" / "mcp_app" / "views" / "widgets" / "get-time" / "widget.tsx"

belgie = BelgieExtension(project=PROJECT_ROOT)


class TimeResult(TypedDict):
    time: str


@belgie.tool(
    widget=WIDGET,
    name="get-time",
    title="Get Time",
    description="Get the current server time in ISO 8601 format.",
)
def get_time() -> TimeResult:
    return {"time": datetime.now(tz=UTC).isoformat()}


mcp = MCPServer(name="Get Time Server", extensions=[belgie])


def main() -> None:
    uvicorn.run(mcp.streamable_http_app(), host="127.0.0.1", port=3001)


if __name__ == "__main__":
    main()

The complete implementation is in examples/ui/mcp/src/mcp_app/__main__.py.

Widget and generated caller

The widget imports Widget and useToolResult from @belgie/mcp. The generated caller gives the hook an input and output schema:

import { Widget, useToolResult } from "@belgie/mcp";
import { getTime } from "@widgets/tools";

function AppView() {
  const { data, isLoading, execute } = useToolResult(getTime);
  return (
    <main>
      <p>{data?.time ?? (isLoading ? "Waiting..." : "No time returned.")}</p>
      <button onClick={() => void execute()}>Refresh</button>
    </main>
  );
}

export default function GetTime() {
  return (
    <Widget metadata={{ name: "Get Time", version: "1.0.0" }}>
      <AppView />
    </Widget>
  );
}

Run npx belgie-mcp generate against the running endpoint when the tool schema changes. See @belgie/mcp for authentication and --check options.

Production build

Set dev=False in BelgieExtension after building the widget assets, or use the project's production configuration. The extension then reads dist/widgets/get-time/index.html instead of starting a Vite development server.

Variants

See also