starter
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@starterRun the say_hello tool for Alice and open the hello App UI."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 user can then enter a name and update the greeting directly from the App:

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 UIIn this example:
app/tools/say_hello.pycontains the tool and declaresui://starter/hello.htmlin itsAppConfig.app/ui/say_hello/resource.pyregisters that same URI and serves the compiled UI.app/ui/say_hello/src/App.jsxreceives the result and readsstructuredContent.message.app/ui/say_hello/index.htmlandsrc/style.cssdefine what the user sees inside the MCP client.
The tool returns two forms of output:
contentis readable by the model and by clients without MCP Apps support.structured_contentis 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 .envThe 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:
What the tool does.
When the model should use it.
Every argument and its expected meaning.
The return contract, especially fields exposed to the UI.
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.jsThe 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.
4. Link the tool and UI
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.py6. 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 testsAdd 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 8000Available endpoints:
MCP transport:
/mcpManifest:
/api/v1/.well-known/mcp.jsonHealth:
/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:localBefore using the included GitHub workflows, replace:
The
mcp-server-*image repository names.The
your_mcpkey 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.lockandapp/ui/say_hello/package-lock.jsonare generated dependency snapshots. Regenerate them with uv/npm commands instead of editing them.Do not commit
.env, runtime logs,node_modules, Python bytecode, orapp/ui/*/dist.Commit
uv.lockso backend dependency resolution remains repeatable.Commit
package-lock.jsonso UI dependency resolution remains repeatable.Rebuild the UI before local integration testing.
Let the Docker build produce the deployable UI bundle for releases.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
Build multi-tenant apps over MCP. Schemas, CRUD, deploys — access control enforced server-side.
Public, read-only MCP server for FarmNeural company facts, packages, and capabilities.
Build, deploy, and host full-stack web apps from any MCP client. DB, auth, storage, cron included.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceProduction-ready MCP server starter with authentication, observability, and a plugin system for building and deploying MCP servers quickly.MIT
- AlicenseNot gradedqualityDmaintenanceA production-ready FastMCP server template supporting local development with stdio and secure web deployment with HTTPS and OAuth.4MIT
- AlicenseNot gradedqualityBmaintenanceA 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
- FlicenseNot gradedqualityBmaintenanceEnables 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.-