MCP servers behind sign-in
An MCP server you deploy here is an app like any other: nobody reaches it without signing in, and only the people you share it with get in. Turn on AI clients for it, and Claude, Cursor or any client that follows the MCP authorization spec can connect too. The person using the client signs in with their Jiayang Cloud account, approves the client once, and the client then calls the app as them.
It’s on every plan. Your server needs no sign-in code of its own. It gets the same identity token a browser request carries, so requireUser() tells you who is calling, and their role.
Turn it on
Section titled “Turn it on”AI clients can’t connect to an app until someone who can share it turns this on. In the dashboard, open the app, go to Settings, and under AI clients select Turn on. From the CLI:
jiayang app mcp acme/tools onAI clients can connect to acme/tools at https://tools--acme.jiayang-apps.cloud. Each signs in as someone it's shared with, who approves it in a browser.jiayang app mcp acme/tools off turns it off. Every client already connected is disconnected and refused from its next request. Turning it on again brings none of them back: each has to connect again.
The app’s owners, and the workspace’s owners and admins, can turn it on or off. A workspace’s publish policy can keep turning it on, and connecting clients, to the people its owners name. Under such a rule, someone outside the workspace can’t connect a client, even to an app shared with them. The change is in the audit log as app.update.
Connect a client
Section titled “Connect a client”Give the client the app’s address with your server’s path, for example https://tools--acme.jiayang-apps.cloud/mcp. Use the app’s address on jiayang-apps.cloud even if the app has a custom domain: a custom domain takes no token, and doesn’t tell a client where to sign in. In Claude Code:
claude mcp add --transport http tools https://tools--acme.jiayang-apps.cloud/mcpIn Cursor, add it to .cursor/mcp.json:
{ "mcpServers": { "tools": { "url": "https://tools--acme.jiayang-apps.cloud/mcp" } }}Cursor signs in through an address on your own computer. A Cursor version that offers only its cursor:// address can’t connect: update Cursor.
In Claude on the web or the desktop app, add it as a custom connector with the same address.
The first time the client calls the app, it opens a browser at the dashboard. Sign in if you aren’t already. The page shows the client’s name, where it will send you back to, the app and the account you’re signed in as. Select Approve, and the browser goes back to the client, which is now connected.
If the page says the app isn’t shared with you, you’re signed in as someone without access. Sign in as someone it’s shared with, or ask for access.
If the client asked for something we don’t do, the page says what, and takes you back to the client only when you select the button.
A client’s name is whatever it says it is. The page says whether the client published its details on a site of its own, and warns you when it sends you back to a program on your own computer. Approve only a client you started connecting yourself, a moment ago.
Anyone can register a client, or publish one on a site of their own, under any name, so a link to this page can come from anyone. When a client would send you back, with access, to a website, and you’ve never approved that client for this app sending access there, the page names that site: “Approving sends access to” the site. It does the same every time for a client that publishes its details, approved before or not. Approving then takes one more step: tick I started this connection first. If you didn’t start it a moment ago from an AI tool you trust, deny it.
For a client we registered, the page counts a client and site as ones you’ve used only for the same client, the same app and the same address exactly, path and port included, from a connection you approved in the last 60 days that is still live, ran out, or that a newer connection of yours replaced. So when a connection runs out after its 30 days and the client asks again, the page doesn’t warn. Another app in the same workspace doesn’t count: someone could share a harmless app with you, ask you to approve their client for it, and so skip the warning for a sensitive app beside it. Once a connection to that address in the workspace has ended any other way, the page warns again for it: when you disconnect it, when the client hands its access back, when a manager ends it, when a code is used twice, or when the workspace stops letting the client in. Approving it again, with the extra step, makes it one you’ve used from then on. We keep a connection that ended any of those ways for at least 60 days after it ended, so the warning lasts as long as any approval from before it would count.
The warning can’t tell who started a connection. Say you’ve connected Claude on the web to an app before. Someone else can start connecting Claude to that app from their own account and send you the link it opens, and if you approve it, it’s their Claude account that gets access. When the platform registers a client for each connection, theirs is a client you’ve never approved, and the page warns. When it publishes its details, its one client id is everyone’s, so the page can’t tell theirs from yours: it warns and asks for the extra step every time, even for a client you’ve approved before. It still can’t stop you approving theirs, and a list that lets the client in doesn’t either. A platform that registers one client and shares its id looks to the page like a single install, so once you’ve approved it, it doesn’t warn. Approve only a connection you started yourself, a moment ago.
If you deny a client that would send you back to a site you’ve never sent access to, nothing is connected and you stay on the page. It takes you to that site to tell the client no only when you select the button, which names the site.
If the app’s workspace lets in only the clients it has approved, and this isn’t one, the page says so, and names the site it would send access to and the client’s id. For a client that publishes its details, it names only the host they’re published on: the rest of that address is the publisher’s to write, and could say anything. Nothing is connected, and the page never takes you to the client’s site, not even to tell it no or to say its request was wrong: nobody in the workspace has approved that site. The name a client gives is its own word, so ask an owner or admin of the workspace to add it only if you started connecting it yourself.
What a connected client can do
Section titled “What a connected client can do”A client reaches the one app you approved it for, as you, with your role there. It can’t reach your other apps, the dashboard or the API.
Everything that limits you limits the client. If the app stops being shared with you, the client is disconnected and refused from its next request. Open connections, such as a stream, end within 15 seconds. Sharing the app with you again doesn’t bring the client back: connect it again.
If the app’s workspace wants its people signed in through its own provider, approve the client from a browser signed in that way. A client approved from any other sign-in is refused, including one connected before the workspace started asking.
A connection lasts 30 days. The client signs in again after that. Its access tokens last an hour, and it gets new ones without asking you.
You can have 50 connections at a time, and three from one client to one app. Connecting past either ends your oldest one.
Choose which clients can connect
Section titled “Choose which clients can connect”By default, anyone an app is shared with can connect any AI client to it, and the consent page’s warning is all that stands between a link someone sends and your apps. A workspace’s owners and admins can let in only the clients they approve, and change what the list holds. Only an owner can turn the list off again, which lets any client in. Changing it asks for a sign-in from the last 15 minutes, as single sign-on does, and the CLI can’t change it. If your apps hold anything sensitive, turn the list on. In the dashboard, go to the workspace’s Settings, and under AI clients select Only the clients on this list. Add each entry on its own line:
loopback, which lets in every client that sends you back only to your own computer (localhost,127.0.0.1or[::1]). Claude Code, Cursor and Codex connect this way, so their codes can’t reach anyone else’s site. Most of them register a new client for each install, so this is the way to let them in.- The address a client publishes its details at, which lets in that one client. Claude Code publishes its details at
https://claude.ai/oauth/claude-code-client-metadata(checked in October 2026), and uses them with servers that take them, as this one does. Prefer this to a host when the client publishes its details. - A host, such as
app.example.com. It covers addresses to send you back to that are https on that host, and doesn’t cover anything on another host, which includes the names under it:app.example.comdoesn’t covereu.app.example.com. A client that publishes its details on a listed host is covered on your own computer too. - A client’s id,
oc_and 16 letters and digits, from the connections list or from the page that refused it. It lets in one registered client, which is usually one install of one tool. A client that registers again gets a new id.
A client gets in when the list covers every address it may send you back to. Some send you back to your own computer or to a website of their own, VS Code among them, and some to more than one website: list loopback and each of those hosts, and they get in. List some but not all, and they don’t. The page that refused a client names the site it would have sent access to.
VS Code sends you back to https://vscode.dev/redirect or https://insiders.vscode.dev/redirect, which pass the code on to an address the request carries. We checked in October 2026 that they pass it only to your own computer (localhost, 127.0.0.1 or a name under .localhost), to VS Code itself, or to vscode.dev and github.dev, and refuse any other address. So listing vscode.dev and insiders.vscode.dev lets no one else’s site take a code, but it does let in any client that sends you back there, whatever it calls itself.
A host entry trusts every address on that host, at any path. If anyone can take a callback there, or the host forwards a visitor on to another address with what came with them (an open redirect), a client sending codes there can hand them to someone else. List only a host whose owner you trust with that, never one where anyone can put a file, take a callback or get a name. The list refuses the common ones: public suffixes such as github.io, herokuapp.com or co.uk themselves, file hosts such as raw.githubusercontent.com, gitlab.com, huggingface.co and cdn.jsdelivr.net, cloud storage, IPFS gateways, webhook relays, request catchers and tunnels. A name of your own on one of those platforms, such as you.github.io, can be listed. For a client published on a host you can’t list, list its own address instead.
The settings show what each id on the list calls itself, where it sends people back to and when it registered, once someone has tried to connect that client to one of the workspace’s apps, even if the list refused it. An owner or admin can do that themselves for any id they have, on one of their own apps, and client ids aren’t secret: they’re in consent links and in audit logs. So treat a client’s name, the hosts it sends people back to and when it registered as something any workspace’s owner can read once they have its id. Its full addresses aren’t shown to anyone, and a client nobody has named to your workspace isn’t described in its settings. When you add an id or an address, the first Save only reads what that client is and shows it beside the entry, marked new. Select Save again to add it. A client’s name proves nothing: anyone can register one called “Claude Code”. Check where it sends people back before you add it. A registered client nobody has used is deleted a day after it registered, listed or not: anyone can list any id, and a list that kept one would keep a link someone sent working for as long as they liked. Its id then shows as gone, and lets nobody in. The client registers again with a new id when someone next connects it, so prefer loopback or a host to an id where they cover it.
The list holds up to 100 entries.
Through the API, GET /v1/workspaces/<workspace>/ai-clients reads the setting. PUT on the same path sets it: {"mode": "listed", "allowed": ["loopback", "https://claude.ai/oauth/claude-code-client-metadata"]} sets the mode and the whole list. Leave allowed out to change the mode alone and keep the list as it is, so {"mode": "anyone"} lets any client in again and keeps the list for later. POST /v1/workspaces/<workspace>/ai-clients/preview, with the same body, changes nothing and answers with cuts, the clients whose connections that change would end, with how many connections and people, and allowed, the list as it would be, with what each client an entry names calls itself and the hosts it sends people back to, once someone has tried to connect it to one of the workspace’s apps. All three are for a browser signed in as an owner or admin, not the CLI, and setting the mode back to anyone is for an owner. The database holds the same lines.
Turning the list on, or taking a client off it, disconnects every client of the workspace’s apps that the list now leaves out, and each is refused from its next request. The settings name those clients before you save. Codes approved for them and not yet used stop working too. Putting a client back on the list doesn’t reconnect anyone: they connect again. Each change is in the audit log as ai_clients.set, ai_clients.allow or ai_clients.remove, and each connection it ends as oauth.revoke with why client_not_allowed.
See and disconnect clients
Section titled “See and disconnect clients”Your connected clients are on the dashboard’s Sessions page, under AI clients, with the app each one reaches and when it was last used. Select Disconnect to end one. It’s refused from its next request.
From the CLI:
jiayang connections listjiayang connections disconnect <id>jiayang connections disconnect --allYour clients are disconnected too when:
- you choose Sign out everywhere on the Sessions page, or run
jiayang logout --everywhere; - the app stops being shared with you: it’s unshared, you leave the group or the workspace that let you in, it’s closed to the workspace, or the workspace stops letting your email domain in;
- you unlink the Google or Microsoft account, or the workspace removes the provider, that signed in the browser you approved the client from;
- you remove a passkey, and you approved the client from a browser signed in with a passkey;
- someone who can share the app turns AI clients off for it;
- you delete your account, or the app or its workspace is deleted;
- the workspace starts asking for its own provider and you approved the client from another sign-in. It’s refused from its next request, and disconnected the next time it asks for new tokens;
- the workspace lets in only the clients on its list, and this one isn’t on it.
A client can also disconnect itself. The app’s owners, and the workspace’s owners and admins, can list every client connected to the app and end any of them:
jiayang connections list --app acme/toolsjiayang connections disconnect <id> --app acme/toolsThrough the API, GET /v1/workspaces/<workspace>/apps/<app>/connections lists them, newest first and 500 at a time, with ?before=<id> for the ones after the last on a page, and DELETE on /connections/<id> ends one.
In the audit log
Section titled “In the audit log”Each step is in the workspace’s audit log:
| Action | When |
|---|---|
oauth.connect |
Someone approved a client for an app. |
oauth.token |
A connected client was given new tokens. |
oauth.revoke |
A connection was ended: by its person, by the client, by someone who manages the app, because the app stopped being shared with its person, because AI clients were turned off or the sign-in that approved it went, because the workspace’s own sign-in no longer accepts how the client was approved, because its person’s account, the app or the workspace was deleted, because the workspace no longer lets the client in, or because a newer connection took its place. why in the detail says which. |
oauth.reuse |
A code or refresh token was used twice, so someone else may have a copy. The connection ended. |
Each request a client makes is in the log too, with the person’s email and mcp as how they came in.
What your server sees
Section titled “What your server sees”Each request reaches your server with the identity token in X-Jiayang-Identity, as it would from a browser. Check it with the SDK, as in How access works. The client’s access token never reaches your server: we take it off the request before your server sees it.
Who signs a client in is ours to say, so a WWW-Authenticate: Bearer header your server sends to a client is taken off. Answer 401 or 403 for your own reasons as you like.
Some paths on the app’s address are ours, not your server’s: /.well-known/oauth-protected-resource and every path under it, which tell a client where to sign in, and /.jiayang/session, where a browser’s sign-in ends.
For client authors
Section titled “For client authors”We follow the MCP authorization spec, revision 2026-07-28, and the OAuth 2.1 draft it links to, draft-ietf-oauth-v2-1-13. An app’s address answers a call with no credential with 401 and:
WWW-Authenticate: Bearer resource_metadata="https://tools--acme.jiayang-apps.cloud/.well-known/oauth-protected-resource"That answer is the same for every app’s address, so it says nothing about which apps exist or which take AI clients.
- The protected resource metadata (RFC 9728) names
https://app.jiayang.cloudas the authorization server. Its metadata (RFC 8414) is athttps://app.jiayang.cloud/.well-known/oauth-authorization-server. - Clients register by publishing a Client ID Metadata Document at an https address, which we read and keep for up to a day, or with dynamic client registration (RFC 7591). Only public clients: we never issue a client secret.
- The authorization code flow with PKCE, and only the
S256method. - Redirect addresses are matched exactly. One on
http://localhost,http://127.0.0.1orhttp://[::1]may use any port (RFC 8252). Any other has to be https. Write each one plainly, as a URL parser writes it: a lowercase scheme and host, a port as plain digits if you write one (:443is fine,:0443isn’t), and127.0.0.1rather than another way of writing it, such as127.1. An address that is none of these, such ascursor://, is left out of the registration, and the answer lists the ones kept. A client with none left can’t register. - Send
resourcewith the app’s address (RFC 8707) in both the authorization and the token request. A token is for that one app, at its address on jiayang-apps.cloud. An authorization request that leaves it out, or names something that isn’t an app, gets its error on the consent page, not at your redirect address: with no app, there’s no workspace to say whether your client is let in. - A client registered with one redirect address may leave
redirect_uriout of the authorization request. Then leave it out of the token request too, or send that same address. A client that sent it sends it again, the same. - The authorization response carries
iss(RFC 9207). - Access tokens start with
jyo_and last an hour. Send them asAuthorization: Bearerand nowhere else. Our tokens all start withjy, a letter for their kind and_. A request is refused, whatever else comes with it, when its path or query holds the start of one, as written or percent-encoded, or when a whole one, as written, percent-encoded or inside base64 (as inBasiccredentials), is inAuthorizationas anything but a singleBearer jyo_token, or in any other header butCookieandReferer, such asProxy-AuthorizationorX-Api-Key. A cookie holding a whole one, or aRefererholding the start of one (or a whole one inside base64), is taken off on the way to the app instead, and the request goes on. Refresh tokens start withjyr_, last until the connection ends, and work once. Using one twice ends the connection. - Hand a token back at
https://app.jiayang.cloud/v1/oauth/revoke(RFC 7009).
Some limits keep the open endpoints working for everyone:
- One address, or IPv6 /64, registers at most 30 clients an hour.
- When 20,000 clients of one kind have registered in the last hour, from everyone, one more of that kind is answered
429withRetry-After: 60. Clients that send you back only to your own computer are one kind, and every other client is the other, so a flood of one kind never holds back the other. - Past half of that, an IPv6 /48 that has registered 240 clients in the hour is answered
429too. Short of it, a /48 is held to no more than its /64s are, so a few people in a carrier’s or a cloud’s /48 can’t shut out everyone else in it. - An address whose client has connected someone in the last 30 days is held back only by its own 30. Any connection used to count, however old: now a hosted client nobody has used for 30 days waits during a flood like a new one, until it connects someone again. That says nothing about whether to trust it: anyone can connect a client of their own to an app of their own, and someone who does that from many addresses can fill a kind’s 20,000 for an hour. These limits keep registration working for everyone during a flood. They don’t stop a single client made to trick someone: the consent page’s warning and a workspace’s list of clients are what do that.
- A registered client that hasn’t connected anyone a day after it registered is deleted, and so is one nobody has used for 90 days once it has no connection left. Register again to use it. That keeps the table small, and doesn’t bound how long a link someone sent you stays a risk: whoever registered a client can connect it once to an app of their own, and it’s then kept for 90 days after its last use. There’s no way to delete a registered client yourself. Disconnect it and it reaches nothing until someone connects it again, and a workspace’s list of clients keeps it out of that workspace’s apps. Write to support@jiayang.cloud to have one deleted sooner.
- When the consent page reads a client’s published details, one person causes at most 20 reads a minute. One host is read at most 30 times a minute for clients we’ve read before. Addresses we haven’t are counted apart: at most 10 a minute on one host for each person, and 120 for everyone. An address that didn’t answer, or whose details didn’t add up, isn’t read again for a minute. Details read before are used for up to a day past their time while their address can’t be read.
- Details are never read from jiayang.cloud, jiayang-apps.cloud or any name under them.
- A token that isn’t valid (made up, revoked, expired, or for another app or an address with no app) counts against the address it came from. Past 60 in a minute, that address’s tokens are answered
429for a minute, without being checked. A valid token is never counted, even when the app or its workspace turns it away. - The token, registration and revocation endpoints take a limited number of calls a minute from each address, and answer
429withRetry-Afterpast it.
Clients that run inside a web page can’t call an app yet, since the app’s address doesn’t answer a browser’s cross-origin preflight for it.