Skip to main content
Glama

Twynity FastMCP + MCP Apps starter template

A production-shaped starter for building a Python FastMCP server with an embedded MCP App.

The template retains Twynity's authentication, licensing, usage reporting, manifest and health routes, container setup, and deployment workflow. The example feature is intentionally small: a say_hello tool renders Hello, <name>! inside an interactive, client-themed React UI.

Example output

Calling say_hello opens the MCP App in the client's available UI space:

The initial Hello World tool result and MCP App

The user can then enter a name and update the greeting directly from the App:

The MCP App updated with a personalised greeting

Related MCP server: mcp-server-template

Understand the tool-to-UI flow

An MCP App consists of a tool and a UI resource joined by the same ui:// URI:

Model calls say_hello
        |
        v
Tool returns content + structured_content
        |
        | AppConfig(resource_uri="ui://starter/hello.html")
        v
Client loads the matching ui:// resource
        |
        v
app.ontoolresult receives structuredContent and updates the UI

In this example:

  • app/tools/say_hello.py contains the tool and declares ui://starter/hello.html in its AppConfig.

  • app/ui/say_hello/resource.py registers that same URI and serves the compiled UI.

  • app/ui/say_hello/src/App.jsx receives the result and reads structuredContent.message.

  • app/ui/say_hello/index.html and src/style.css define what the user sees inside the MCP client.

The tool returns two forms of output:

  • content is readable by the model and by clients without MCP Apps support.

  • structured_content is the stable JSON contract consumed by the UI.

1. Configure the starter

Set mcp_name in app/config.py. It is deliberately empty and marked with a Put your MCP name here comment. Until it is set, the application uses starter as a runnable fallback.

Copy the environment example and replace its placeholder values:

cp .env.example .env

The production authentication, usage, and licensing integrations require working service URLs and credentials.

2. Build a tool

Use app/tools/say_hello.py as the pattern. A useful LLM-facing tool docstring should explain:

  1. What the tool does.

  2. When the model should use it.

  3. Every argument and its expected meaning.

  4. The return contract, especially fields exposed to the UI.

  5. A short representative example.

Register the tool in app/main.py. Return a ToolResult when you need explicit control over both model-readable content and UI-readable structured data:

return ToolResult(
    content="A useful summary for the model",
    structured_content={"message": "Data for the UI"},
)

Treat structured_content as an API contract. If the Python field name changes, update the UI that reads it and the tests that protect it.

3. Build the UI

Every UI-enabled tool gets a matching folder under app/ui/. For example, app/tools/say_hello.py is paired with app/ui/say_hello/:

app/ui/say_hello/
|-- resource.py      # Registers the ui:// resource
|-- index.html       # Document structure; your UI goes here
|-- src/App.jsx      # React UI, MCP hooks, and tool-result handling
|-- src/main.jsx     # React entry point
|-- src/style.css    # Client-aware presentation
|-- package.json
`-- vite.config.js

The example uses the official @modelcontextprotocol/ext-apps/react package. useApp owns the App connection, useHostStyleVariables applies the client's theme and CSS variables, useHostFonts installs client-provided font rules, and useDocumentTheme exposes the active theme reactively. Always provide CSS fallbacks because hosts may expose different subsets of styling information. React escapes rendered string values by default; do not bypass that protection with dangerouslySetInnerHTML for tool-provided content.

The name input is a controlled React field. Submitting the form calls say_hello through app.callServerTool, then renders the returned structuredContent.message. Tools called from their UI need "app" in their AppConfig.visibility; this example uses ["model", "app"] so both the model and UI can call it.

The example also enables useApp({ autoResize: true }). Its document and root styles provide a useful intrinsic minimum height and stretch to the host's available iframe height, allowing the host to resize the App dynamically.

Choose one stable URI and use it in both places:

VIEW_URI = "ui://starter/hello.html"

@mcp.tool(app=AppConfig(resource_uri=VIEW_URI))
def your_tool(...):
    ...

@mcp.resource(VIEW_URI, app=AppConfig())
def your_view():
    ...

The MCP client discovers the URI in the tool metadata, reads the matching resource, renders it in a sandboxed iframe, and forwards tool results to the App SDK's ontoolresult handler.

Also repeat the URI in the returned ToolResult.meta:

return ToolResult(
    content="A useful summary for the model",
    structured_content={"message": "Data for the UI"},
    meta={
        "ui": {"resourceUri": VIEW_URI},
        "ui/resourceUri": VIEW_URI,
    },
)

The nested value is the current MCP Apps representation and the flat value is retained for compatibility. Twynity clients inspect this response metadata to select the renderer immediately, avoiding an additional resource-discovery round trip. Keep both values aligned with the URI registered by the resource.

5. Compile the UI

Generated frontend files are not committed. Install Node.js 20.19+ or 22.12+, then compile the UI before running the server directly:

cd app/ui/say_hello
npm ci
npm run build
cd ../../..

This creates app/ui/say_hello/dist/index.html. Running the Python server without it produces an error explaining which build commands are required.

For iterative UI work, rebuild after changes or use Vite directly. FastMCP also provides an MCP Apps preview environment:

fastmcp dev apps app/main.py

6. Install and test the backend

Install uv, then sync the locked runtime and development dependencies. uv creates and manages the project's .venv automatically:

uv sync --locked
uv run pytest -q
uv run ruff check app tests

Add or remove Python packages with uv add <package> and development tools with uv add --dev <package>. Commit both pyproject.toml and uv.lock so local, CI, and container installs resolve to the same versions.

The complete test suite expects the UI compilation step to have run first so it can verify the actual resource served to MCP clients.

7. Run locally

uv run uvicorn app.main:app --host 0.0.0.0 --port 8000

Available endpoints:

  • MCP transport: /mcp

  • Manifest: /api/v1/.well-known/mcp.json

  • Health: /api/v1/health

The /mcp transport requires a bearer token issued by the configured account service. The manifest and health endpoints remain public.

8. Compile and deploy with Docker

The Dockerfile is multi-stage. Its Node stage installs the locked UI dependencies and compiles dist/index.html; its Python stage uses the uv lockfile to install production dependencies and copies only the compiled UI into the runtime image. A local build is:

docker build -t your-mcp:local .
docker run --rm --env-file .env -p 8000:8000 your-mcp:local

Before using the included GitHub workflows, replace:

  • The mcp-server-* image repository names.

  • The your_mcp key used to update the GitOps values file.

  • Environment-specific domains or secrets required by your deployment.

The development, staging, and production workflows retain the Twynity build, registry, and GitOps deployment sequence.

Repository rules

  • uv.lock and app/ui/say_hello/package-lock.json are generated dependency snapshots. Regenerate them with uv/npm commands instead of editing them.

  • Do not commit .env, runtime logs, node_modules, Python bytecode, or app/ui/*/dist.

  • Commit uv.lock so backend dependency resolution remains repeatable.

  • Commit package-lock.json so UI dependency resolution remains repeatable.

  • Rebuild the UI before local integration testing.

  • Let the Docker build produce the deployable UI bundle for releases.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Production-ready MCP server starter with authentication, observability, and a plugin system for building and deploying MCP servers quickly.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A production-ready FastMCP server template supporting local development with stdio and secure web deployment with HTTPS and OAuth.
    4
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    A standard, forkable MCP App server with a Streamable HTTP endpoint, providing a self-contained UI resource and native tools like search_projects for project discovery.
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables developers to build MCP servers with registry-managed tool metadata, runtime hot-reloading, pluggable authentication and authorization, per-audience tool views, and resilient stateless operation.
    -