Give your app an MCP server
An app you build on Cloudrizz can itself be an MCP server. Add one file and ChatGPT, Claude, or any MCP client can connect to your app and call tools you define — signed in as your app's own end-users. Cloudrizz generates the entire OAuth authorization server and MCP transport for you.
Two different MCP servers
Don't confuse this with the Cloudrizz MCP server you connect to in order to build apps:
- The Cloudrizz MCP server (
mcp.cloudrizz.com) is how you build and deploy apps from your assistant. - Your app's MCP server (this page) runs on your app's own domain and lets your users' assistants call the tools your app exposes.
Enable it: ship api/mcp/tools.js
There's no setting to flip. When your app contains a file at api/mcp/tools.js whose default export is an array of tools, Cloudrizz injects the MCP runtime at deploy time. Apps without that file pay nothing.
Each tool is a plain object:
// api/mcp/tools.js
export default [
{
name: "list_notes",
description: "List the signed-in user's notes.",
// JSON Schema describing the arguments (optional).
inputSchema: { type: "object", properties: {} },
async handler(args, { user, env }) {
const { results } = await env.DB
.prepare("SELECT id, body FROM notes WHERE user_id = ?")
.bind(user.id)
.all();
return results;
},
},
{
name: "add_note",
description: "Create a note for the signed-in user.",
inputSchema: {
type: "object",
properties: { body: { type: "string" } },
required: ["body"],
},
async handler({ body }, { user, env }) {
await env.DB
.prepare("INSERT INTO notes (user_id, body) VALUES (?, ?)")
.bind(user.id, body)
.run();
return "Note saved.";
},
},
];The tool object
name— unique tool name the assistant calls.description— what the tool does (the model reads this to decide when to use it).inputSchema— JSON Schema for the arguments. Optional; defaults to an empty object.scope— optional. If set, the tool is only callable when the access token was granted that scope.handler(args, ctx)— your code. Receives the parsedargsand a context object.
The handler context gives you everything you need to act as the user:
ctx.user— the authenticated end-user (id,email,name,emailVerified,twoFactorEnabled,createdAt). Scope queries touser.id.ctx.env— your app bindings (env.DB,env.STORAGE,env.AUTH, etc.).ctx.request— the incoming request.ctx.scopes— the scopes granted to this token.
Return a string or any JSON-serializable value and Cloudrizz wraps it as MCP text content. Return an object with a content array to control the MCP result yourself. Throwing inside a handler is reported to the model as a tool error, so it can react.
What Cloudrizz serves for you
Once api/mcp/tools.js is present, your deployed app answers these routes on its own domain — you write none of this:
/mcp— the MCP endpoint (Streamable HTTP). This is the URL your users add to ChatGPT or Claude./.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resource— discovery metadata./oauth/register— Dynamic Client Registration (RFC 7591)./oauth/authorize— sign-in + consent screen, backed byenv.AUTH(your app's own users, including email OTP if 2FA is on)./oauth/token— OAuth 2.1 token endpoint with PKCE and refresh rotation.
Auth state lives in your app's database under the reserved _cr_oauth_* tables (created on first use). Access and refresh tokens are stored hashed, never in plaintext. Tools require a valid token — an unauthenticated request to /mcp returns a 401 that triggers the client's OAuth flow automatically.
To see which clients have connected and remove one, use the manage_oauth_clients tool (or the app's dashboard): list them, revoke a client's tokens to force a reconnect, or delete a stale or accidentally-registered client entirely.
Sign in with Microsoft or Google
By default the sign-in screen uses your app's built-in accounts (email + password via env.AUTH). If your users live in Microsoft 365 / Entra ID or Google Workspace, add a “Sign in with Microsoft” and/or “Sign in with Google” button so nobody needs a separate password just to connect their assistant. Each provider is enabled by two environment variables on your app (ask your assistant, or use the dashboard's Environment card):
- Microsoft: create an app registration in your Entra admin center with
https://<your-app-domain>/oauth/microsoft/callbackas a Web redirect URI + a client secret. SetCR_AUTH_MS_CLIENT_IDandCR_AUTH_MS_CLIENT_SECRET(as a secret). Then pick Supported account types deliberately — see “One organization or many?” below. - Google: create an OAuth client (type “Web application”) in Google Cloud Console with
https://<your-app-domain>/oauth/google/callbackas an authorized redirect URI. SetCR_AUTH_GOOGLE_CLIENT_IDandCR_AUTH_GOOGLE_CLIENT_SECRET(as a secret). Optional:CR_AUTH_GOOGLE_HD= your Workspace domain (e.g.firma.no) to allow only accounts from it. - Redeploy. The buttons appear on the sign-in screen automatically — register one redirect URI per domain your app serves on.
A user who signs in this way is matched to your app's account with the same email (or one is created, pre-verified) — for Microsoft this depends on your setup, see the next section. Their Microsoft/Google sign-in — including your organization's MFA — replaces the app's own password and email-code steps.
Branding the sign-in screen
The built-in sign-in & consent screen can carry your own name, logo, and color instead of the default look. Set any of these as app environment variables and redeploy:
CR_AUTH_BRAND_NAME— the name shown in “Sign in to …” (defaults to your app name).CR_AUTH_BRAND_SUBTITLE— replaces the line under the title.CR_AUTH_BRAND_LOGO_URL— anhttps://image URL shown above the title (other URLs are ignored).CR_AUTH_BRAND_ACCENT— accent color for the primary button (a hex like#0055ffor anrgb(...)value).
To make it SSO-only — hide the email/password fields and offer just your configured Microsoft/Google button — set CR_AUTH_SSO_ONLY to true. It only takes effect when at least one provider is configured, so you can't accidentally lock everyone out.
Microsoft: one organization or many?
When you create the Entra app registration you choose who can sign in under Supported account types. Pick the setup that matches your app:
- Just your own organization: choose Accounts in this organizational directory only and set
CR_AUTH_MS_TENANT= your tenant ID. Only people in your directory can sign in, and anyone with an existing app account under the same email is linked to it automatically. - Many organizations (e.g. your app serves several customer companies): choose Accounts in any organizational directory, leave
CR_AUTH_MS_TENANTunset, and in the registration's Token configuration add these optional claims to the ID token:email,xms_edov,verified_primary_emailandverified_secondary_email(accept the Microsoft Graph email permission it suggests). These claims are Microsoft's proof that the address really belongs to the person signing in.
Why it matters: unlike Google, a Microsoft sign-in doesn't automatically prove the user owns the email address on the account — without proof, someone could set up their own directory with your user's address and walk into their account. So without a tenant lock, your app only links a Microsoft sign-in to an existing password account when the token carries that proof (the optional claims above). If the proof is missing, the sign-in is refused for that account rather than merged, and the user is asked to use their password instead. Brand-new users (no existing account) can always sign in either way.
How your users connect
Give users your app's MCP URL — your domain plus /mcp:
https://your-app.example.com/mcpThey add it as a custom connector / remote MCP server exactly like they would the Cloudrizz one (see Connect ChatGPT & Claude). The first connection opens your app's sign-in screen; after they authorize, their assistant can call your tools as them.
Changing tools after users have connected
When you add, rename, or remove a tool in api/mcp/tools.js and redeploy, your app serves the new tool set immediately— there is no cache on our side. But an assistant that's already connected may keep showing the old list for a while. That's the MCP client's doing: ChatGPT and Claude fetch a connector's tool list when the connection is established and cache it for the session, and the MCP protocol has no way for a plain request/response server like this one to push a “tools changed” signal to an already-open connection.
To make the assistant pick up your new tools, refresh the connection:
- Claude: open Settings → Connectors, and toggle your app's connector off and on again (or remove and re-add it) — or simply start a new chat, which reconnects.
- ChatGPT: reconnect the connector in Settings, or start a new conversation.
Build it from chat
In practice you don't hand-write any of this. Ask your assistant something like “add an MCP server to my app exposing tools to list and create notes” — it creates api/mcp/tools.js with edit_file, then save_version and deploy_app. See Build & deploy an app.