Build & publish an MCP app
Scaffold with FastAPI Cloud, add fastapi-mcp and store auth, deploy your MCP server, and submit for catalog review. Users sign in once through the MCP Store — your app verifies installs before serving tools.
Recommended stack
FastAPI Cloud
Scaffold, deploy, and host your MCP server — one CLI workflow
fastapi-mcp
Expose MCP tools as FastAPI routes at /mcp
Store auth
Verify Supabase JWTs + install status on every request
MCP Store
Catalog, OAuth, and shared user identity
What you own vs. what the store owns
You deploy & maintain
- • MCP tools and business logic
- • Your FastAPI Cloud app at
mcp_url - • Scaling and uptime of your server
MCP Store provides
- • User accounts & OAuth for MCP clients
- • Sign-in UI at
/mcp-store/oauth - • Catalog listing after review
- • Install records — who may use your app
1. Create your FastAPI Cloud project
Start with the FastAPI Cloud quick start. This scaffolds a project with fastapi[standard] (includes the deploy CLI) and a working app/main.py.
uvx fastapi-new myapp
cd myapp
source .venv/bin/activate # Windows: .venv\Scripts\activateYou'll need
- • uv installed
- • A FastAPI Cloud account (created on first
fastapi deploy)
2. Add fastapi-mcp
Install MCP dependencies, then replace app/main.py with the wired version in the next section. Every tool route uses Depends(require_installed_user) so only users who installed your app from the catalog can call tools.
uv add fastapi-mcp httpx pyjwtfrom fastapi import Depends, FastAPI
from fastapi_mcp import AuthConfig, FastApiMCP
from app.mcp_store_auth import StoreUser, require_installed_user
import os
APP_SLUG = os.environ["APP_SLUG"]
STORE_URL = os.environ["STORE_URL"].rstrip("/")
app = FastAPI(title="My App", version="0.1.0")
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok", "app": APP_SLUG}
@app.get("/my_tool", operation_id="my_tool", summary="My first tool")
async def my_tool(user: StoreUser = Depends(require_installed_user)):
return {"message": f"Hello {user.email or user.id}"}
mcp = FastApiMCP(
app,
name="My App",
auth_config=AuthConfig(
issuer=STORE_URL,
authorize_url=f"{STORE_URL}/oauth/authorize?app={APP_SLUG}",
oauth_metadata_url=f"{STORE_URL}/.well-known/oauth-authorization-server",
client_id="mcp-store",
client_secret="local-dev-secret",
dependencies=[Depends(require_installed_user)],
setup_proxies=True,
setup_fake_dynamic_registration=True,
),
)
mcp.mount_http() # → /mcp3. Store auth module
Add app/mcp_store_auth.py. It verifies JWTs from the MCP Store's Supabase project and calls the store install-check endpoint before your tools run. Required on every MCP app.
"""Save as app/mcp_store_auth.py"""
from __future__ import annotations
import os
from dataclasses import dataclass
from functools import lru_cache
from uuid import UUID
import httpx
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jwt import PyJWKClient
_bearer = HTTPBearer(auto_error=False)
APP_SLUG = os.environ["APP_SLUG"]
STORE_URL = os.environ["STORE_URL"].rstrip("/")
SUPABASE_URL = os.environ["SUPABASE_URL"].rstrip("/")
@dataclass
class StoreUser:
id: UUID
email: str | None
@lru_cache
def _jwks_client() -> PyJWKClient:
return PyJWKClient(f"{SUPABASE_URL}/auth/v1/.well-known/jwks.json")
def verify_supabase_jwt(token: str) -> StoreUser:
try:
signing_key = _jwks_client().get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["ES256", "HS256"],
audience="authenticated",
)
except jwt.PyJWTError as exc:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token.") from exc
sub = payload.get("sub")
if not sub:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token subject.")
return StoreUser(id=UUID(sub), email=payload.get("email"))
async def verify_install(token: str) -> StoreUser:
user = verify_supabase_jwt(token)
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"{STORE_URL}/api/v1/auth/verify",
params={"app": APP_SLUG},
headers={"Authorization": f"Bearer {token}"},
)
if response.status_code == status.HTTP_401_UNAUTHORIZED:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token.")
if response.status_code != status.HTTP_200_OK:
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Store verify unavailable.")
data = response.json()
if not data.get("authorized"):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=data.get("reason", "not_installed"))
return user
async def require_installed_user(
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer),
) -> StoreUser:
if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing bearer token.",
headers={"WWW-Authenticate": "Bearer"},
)
return await verify_install(credentials.credentials)Store URLs your app uses
- Store API
https://mcp-store.fastapicloud.dev - OAuth authorize
https://mcp-store.fastapicloud.dev/oauth/authorize?app={APP_SLUG}Users sign in athttps://thinkrecursion.ai/mcp-store/oauthduring MCP client setup. - Install verify
https://mcp-store.fastapicloud.dev/api/v1/auth/verify?app={APP_SLUG} - OAuth metadata
https://mcp-store.fastapicloud.dev/.well-known/oauth-authorization-server
4. Environment variables & Supabase
MCP Store users authenticate through a shared Supabase project. Set SUPABASE_URL to that project URL so your app can verify JWTs via public JWKS — no Supabase secret keys needed in your app. If your tools need their own database, you can additionally connect a Supabase project through the FastAPI Cloud Supabase integration (injects DATABASE_URL). That is separate from store auth.
APP_SLUG=my-app
STORE_URL=http://localhost:8002
SUPABASE_URL=http://127.0.0.1:54321APP_SLUG=my-app
STORE_URL=https://mcp-store.fastapicloud.dev
SUPABASE_URL=https://YOUR_STORE_SUPABASE_PROJECT.supabase.co
APP_ENV=productionAPP_SLUG must match your catalog slug. Contact jackson@thinkrecursion.ai for the production SUPABASE_URLif you don't have it yet.
# Create .env.local in project root (see env vars section)
fastapi dev
# Health: http://127.0.0.1:8000/health
# MCP: http://127.0.0.1:8000/mcp5. Deploy to FastAPI Cloud
Deploy creates your FastAPI Cloud app and gives you a URL like https://myapp.fastapicloud.dev. Set env vars, redeploy, then use https://myapp.fastapicloud.dev/mcp as your catalog mcp_url.
# Creates your FastAPI Cloud app on first run
fastapi deploy
# → https://myapp.fastapicloud.dev
# Set production env vars, then redeploy
fastapi cloud env set APP_SLUG my-app
fastapi cloud env set STORE_URL https://mcp-store.fastapicloud.dev
fastapi cloud env set SUPABASE_URL https://YOUR_STORE_SUPABASE_PROJECT.supabase.co
fastapi cloud env set APP_ENV production
fastapi deploy
# Catalog mcp_url:
# https://myapp.fastapicloud.dev/mcpConfirm: curl https://myapp.fastapicloud.dev/health · Optional: connect GitHub in the FastAPI Cloud dashboard for auto-deploy on push.
6. Write the agent skill file
The MCP URL tells clients where to connect. A SKILL.md tells agents when and how to use your tools. Users get both after installing from the catalog.
---
name: my-app
description: When and how agents should use this MCP app. Be specific.
---
# My App
## When to use
Describe the scenarios where an agent should reach for this app.
## Tools
### my_tool
What it does, required inputs, and example prompts.
## Setup
1. Install from the MCP Store catalog
2. Connect your MCP client to the app's MCP URL
3. Add this SKILL.md to your agent (Cursor skills folder)7. Submit for review
Submit your live mcp_url and SKILL.md content. RecursionAI reviews both before approving. Approved apps appear in the public catalog.
Ready to publish?
Sign in with your MCP Store account and submit slug, name, live mcp_url, and your SKILL.md.