# Set up the juudd connector

This file is setup documentation for an AI assistant that a person has asked to connect to juudd. **The assistant runs the steps below itself; pasting them back to the person for them to run defeats the point.** A person is needed for approving commands their client asks about, for signing in once in a browser, and, in Claude Code, for starting a new session so the new tools load. When you stop for any of those, tell them exactly what to do next and that pasting the same line again afterwards finishes the setup.

juudd puts websites online: you send files, server-side JavaScript and SQL, and the platform hosts them on a subdomain with a Postgres database of their own.

## Install

**Check first whether juudd is already there.** If `hello` is in your tool list, skip to *Sign in* and call it. In Claude Code, run `claude mcp list`: a `juudd` or `plugin:juudd:juudd` line reading *Needs authentication* means install is done and sign-in is what is left, so skip to *Sign in*. A person who pasted the line, restarted and pasted it again is in exactly this state.

Otherwise use the section for the client you are running in, then go to *Sign in*.

**Connector URL:** `https://mcp.juudd.com/mcp`

That is the whole URL, path included. `https://mcp.juudd.com` on its own answers **404** — measured 2026-09-03, along with `POST /mcp` answering 401, which is the server alive and asking for a token. The server's own RFC 9728 metadata names `https://mcp.juudd.com/mcp` as the resource, so if this document and the metadata ever disagree, the metadata is right.

### Claude Code

There is a plugin, and it is the better of the two paths. It adds the connector **and** the deploy rules below, so the refusals in *"What you can do, and what will be refused"* arrive as a skill you already have rather than as errors you learn one at a time.

```sh
claude plugin marketplace add https://juudd.com/marketplace.json
claude plugin install juudd@juudd
```

To add the connector alone:

```sh
claude mcp add --transport http juudd https://mcp.juudd.com/mcp
```

Run each command on its own. Chained into one line (`;`, `&&`, `if ($?)`), Claude Code asks for approval of the whole chain even where the single commands are allowed — measured 2026-09-17.

**The tools do not arrive in the conversation that installed them.** Measured 2026-09-17 on Claude Code 2.1.273: both install commands succeeded and a search for `hello` in the same session found nothing. Tell the person to quit Claude Code, start it again, and paste the same line; *Sign in* below says what happens then.

### Codex

```sh
codex mcp add juudd --url https://mcp.juudd.com/mcp
codex mcp login juudd
```

`codex mcp login` prints the address of juudd's sign-in page; give it to the person to open. It needs the network outside Codex's sandbox: run inside the Windows sandbox on 2026-09-17 it failed with *OAuth metadata discovery failed … error sending request*, and run outside it the same command printed the sign-in address. If you see that error, ask to run the command outside the sandbox. The tools load in the next Codex session.

### Claude Desktop, and claude.ai

These cannot be configured from a file. Tell the user to open **Customize → Connectors**, click **Add**, give it the name `juudd` and paste the connector URL, click **Continue**, keep the two choices marked *Detected*, click **Add**, then **Connect** and sign in. Walked on a Free claude.ai account against production on 2026-09-17; this said Settings → Connectors → Add custom connector until then, and Anthropic's Help Center that day still described a + menu the screen no longer had. <https://juudd.com/claude> walks a person through it, in Thai at <https://juudd.com/th/claude>. `claude_desktop_config.json` will not work for this server — the desktop app ignores remote MCP servers declared there. Custom connectors work on every Claude plan, Free included; a Free plan allows one custom connector, so if the user already has one, they remove it or upgrade.

### ChatGPT

Custom connectors are added on the web, with **Developer mode** turned on, as a developer-mode app pointing at the connector URL. OpenAI's developer-mode guide (read 2026-09-07) puts the toggle under Settings → Security and login and the app under the plus button in the plugins section; if the menu has moved, the two words to look for are *Developer mode* and *connector URL*. Eligible accounts, per that guide: Pro, Plus, Business, Enterprise and Education. Tell the user to do this; it cannot be done from a file.

Measured 2026-09-07: the owner added the connector this way, signed in, and ran the tools end to end. The production request log shows OpenAI's client (`openai-mcp/1.0.0`) authenticated on `/mcp` and answered 200 throughout. Before that day this document refused to list ChatGPT, on the rule that a setup step which fails is worse than one that is absent; the step has now been watched succeeding.

From ChatGPT there is no shell, so `request_upload` (which posts a folder you built) is not the door for a framework project: `start_build` is. Send the project's source and the platform builds it in a sandbox against its own toolchain; see *"A project a framework needs building"* below.

### Cursor — `.cursor/mcp.json`

```json
{
  "mcpServers": {
    "juudd": {
      "url": "https://mcp.juudd.com/mcp"
    }
  }
}
```

A person who would rather click can use Cursor's own installer from the "Install in Cursor" button at <https://juudd.com/#connect>.

### VS Code with GitHub Copilot — `.vscode/mcp.json`

```json
{
  "servers": {
    "juudd": {
      "type": "http",
      "url": "https://mcp.juudd.com/mcp"
    }
  }
}
```

**`servers`, not `mcpServers`, and the `type` is required.** VS Code is the one client here that differs on both, and it reports neither: given the other clients' shape it loads no server, writes nothing to a log and says nothing at all. This document told you to write `mcpServers` until 2026-09-03. `code --add-mcp '{"name":"juudd","type":"http","url":"https://mcp.juudd.com/mcp"}'` does the same thing from a shell, and <https://juudd.com/#connect> has an "Install in VS Code" button for a person.

### Any other client

Add a **remote MCP server over HTTP** at the connector URL. The server advertises its own authorization metadata, so a client that supports remote MCP with OAuth needs nothing else from you — no API key, no token, no header to set by hand.

A client that reads the **official MCP registry** does not need this document at all: this server is published there as `com.juudd/juudd`, carrying the same connector URL (ADR-0041). Published 2026-09-03 and read back the same day; before that the registry answered zero results for this platform.

ChatGPT was absent from this document until 2026-09-07, first on a second-hand report about its authentication handling that did not survive checking (2026-08-31), then because nobody had watched a ChatGPT client finish the flow. Somebody has now; its section is above.

## Sign in

The goal is one call: `hello`, with any name. It answers with the account you are acting as. If it returns an account, you are done.

**A client that has the server but no sign-in yet may give you no juudd tools at all**, so there is no `hello` to call. What starts the sign-in then depends on the client:

- **Claude Code.** Measured 2026-09-17 on 2.1.273: a session started with juudd configured and not signed in listed the server as *needs-auth* and offered zero juudd tools. Tell the person to type `/mcp`, choose juudd, and choose to authenticate; a browser window opens and they sign in once. Then call `hello`, or, if the tools are still missing, ask them to paste the line into a new session.
- **Codex.** `codex mcp login juudd`, which you run (see *Codex* above).
- **Claude Desktop, claude.ai and ChatGPT.** The person adds the connector in the app's settings, and the app takes them through sign-in there. Tell them to finish it and come back.
- **Any other client.** If the juudd tools are in your list, call `hello`; a client that signs in on first use opens the browser then. If they are missing, the client's own MCP menu is where sign-in lives. Tell the person that.

After sign-in the client keeps the token.

## What you can do, and what will be refused

Read this before your first deploy. Every refusal below is deliberate, and each names the alternative in the same sentence — you can usually recover in one turn if you know the rule in advance.

**Twenty-two tools.** `hello`, `deploy_site`, `request_upload`, `start_build`, `build_status`, `publish_deploy`, `list_deploys`, `read_site`, `unpublish_site`, `site_errors`, `list_refusals`, `request_secret`, `list_secrets`, `create_database`, `apply_sql`, `set_database_url`, `reset_database_credential`, `transfer_database`, `database_status`, `claim_domain`, `domain_status`, `release_domain`.

**A project a framework needs building — React or Vue with Vite, Next.js, Nuxt — has two doors, and which one depends on whether you have a shell.**

- **You have a shell (Claude Code, Codex, Cursor, VS Code): build it yourself, then `request_upload`.** Build where the code is (`npm run build`, `nuxt generate`, Next.js with `output: 'export'`), call `request_upload` with the site, and run the short Node script it returns to post the output folder (`dist`, `out`, `.output/public`). Do not carry a built folder through `deploy_site`; a build is too large for a tool call. Vite apps with client-side routes want `--routing spa`; functions under `functions/` go beside the folder with `--functions`.
- **You have no shell (ChatGPT, claude.ai, Claude Desktop): `start_build`.** Send the project's *source* — `package.json`, the framework config, `src/`, `app/`, `pages/`, `public/` — and the platform builds it in an isolated sandbox against a toolchain it pins. Nothing is installed from `package.json`: a dependency outside the toolchain is refused by name, with the whole list in the reply, so read that list before designing the app (Vite, React, Vue, their routers, Next.js, Nuxt, Tailwind, TypeScript). Next.js must have `output: 'export'`. Leave out `node_modules`, lockfiles and `.env`. A quick build finishes inside the reply; a longer one names a job for `build_status` — wait ten seconds or more between calls, and expect the first build for an account to take a minute or two longer while the toolchain is installed. One build at a time per account, three a minute.

Both doors store a version and publish nothing: call `publish_deploy` afterwards. Server code is never built: framework API routes and server rendering do not come across, and plain-JavaScript functions under `functions/` pass through by the rules below.

**`list_refusals` is how you find out what was already refused.** It answers with what this account has been told no about, newest first, in the exact words the refusal was given in. Read it before retrying something that failed in a conversation you were not part of — a deploy the schema gate stopped, a site another account owns, SQL against a database that has been handed over. Successes are not in it; `list_deploys` already shows those.

**A deploy replaces a site, it does not patch one.** `read_site` hands back the file list of a stored version and the contents of any text file in it. That is how a site deployed in an earlier conversation gets changed in a later one, by you, who were not there when it was written: read what is there, edit it, deploy the whole set again. A file left out of the next `deploy_site` is a file removed from the site.

**`unpublish_site` takes a site off the air, and deletes nothing.** Visitors stop getting it immediately; every stored version stays, the database is untouched, and `publish_deploy` puts it back on the version that was live. Use it when the customer wants their site off the internet — a shop that has closed, a page that went out wrong. Deploying a page that says "closed" is not the same act, because the old page is still reachable to anyone holding the URL. Two things do not survive it: the values of the site's secrets, which live on the script it removes and which this platform cannot read or copy, so they have to be set again afterwards — the reply names them; and, on a site whose database was handed over or set by hand, the only copy of its connection string, which is why that one case refuses unless you pass `accept_credential_loss`. A site this platform has taken off the air refuses `deploy_site` and `publish_deploy` and names an address instead: no argument puts it back.

**`site_errors` is how you find out which route failed, and when.** It answers with which of this account’s sites failed in front of visitors recently, or, given a site, which routes failed, in which hour, how many times, and the runtime’s verdict: 503 is a request stopped at a resource limit, 500 is the code throwing or making too many outbound requests, and those two cannot be told apart. It is not what the function printed — there is still no way to read that. The record is best-effort and kept seven days. When a customer says a route stopped working, call this before `read_site`; after a deploy, call it again to see whether the route is still failing.

Some are absent rather than present-and-failing when the deployment has not configured what they need. Read the tool list you were given; do not assume a tool exists because it is named here.

### Writing server code

1. **No bare imports, with exactly one exception.** `import { neon } from '@neondatabase/serverless'` is allowed. Every other bare specifier is refused — `hono`, `express`, anything from npm. There is no install step and no bundler, so an import that is not there at deploy time is not there at runtime either. **This is the rule you are most likely to break**, because the framework idiom is what your training data is densest in. Relative imports between your own files are fine.
2. **Use the Workers fetch shape, not the Pages one.**
   ```js
   export default { async fetch(request, env) { return new Response('ok'); } }
   ```
   `onRequest(context)` is Pages' vocabulary and is refused by name. A bare `export default async function (request, env)` also works, but prefer the object form — it is the documented one.
3. **Server functions are plain JavaScript.** A `.ts` or `.jsx` file is refused by extension, before anything parses it. Write `.js` or `.mjs`. The code's own comment on this rule is *"Models write `.ts` by reflex"*, so treat it as aimed at you.
4. **Functions live under `functions/`, and the path spells the route.** `functions/api/notes.js` answers `/api/notes`. There is no catch-all — a `/*` route is refused.

### Databases

5. **A table cannot be deployed without a row-level security policy.** The platform reads the schema at deploy time and refuses a table that has none. Write the policy in the same `apply_sql` call that creates the table.
6. **Five kinds of SQL statement are refused before the transaction opens**, by text match: `DISABLE ROW LEVEL SECURITY`, granting `BYPASSRLS` or `SUPERUSER`, `GRANT`/`REVOKE` naming the app role, `DROP`/`ALTER ROLE` on the app role, and `ALTER DEFAULT PRIVILEGES`. Each would expose a live table in the window between deploys, when the deploy-time check is not watching. An ordinary grant to an ordinary role is not refused — only the app role is protected.

### Ownership

7. **You can only act on sites your account owns.** Every tool taking a `site` checks first. A name someone else holds is refused, and so is a site with stored history and no owner.

### Secrets

8. **An API key never goes through you.** Deployed code is public, so a key does not belong in it — and a key in a tool call is a key in this transcript and in every log between here and there, so no tool takes one. When a site needs a Stripe key, a Resend key, a webhook signing secret, anything of that shape: write the code that reads `env.STRIPE_SECRET_KEY`, deploy it, call `request_secret` with the site and the name, and send the customer to the link it returns. They paste the value in the panel; the site is republished to carry it; every later deploy inherits it without anyone reading it. `list_secrets` says whether the value has arrived. Never ask the customer to paste a key into the conversation, and never put one in a file you deploy. `DATABASE_URL` is not a secret you request — `deploy_site` provides it.

### Five things the platform will not do

Do not promise these. Each carries its reason, because most are cheaper to work with than to work around, and one of them is a guarantee rather than a gap.

1. **It will not install your dependencies.** A build here (`start_build`) runs against a toolchain this platform pins and nothing else; a `package.json` naming anything outside it is refused by name, with the whole list in the reply, so read that list before you design the app. With a shell, build it yourself and send the output through `request_upload`. Either way, what a build cannot produce it does not get: server rendering and framework API routes stay behind, and become functions under `functions/`.
2. **It will not show what a function printed.** `site_errors` says which routes failed, when, how many times, and whether the runtime stopped the request or the code threw; it cannot show a stack trace or anything a function logged. What a request returns is still what you have to debug from, so write your functions to answer with the thing that went wrong.
3. **It will not delete anything, and this one is a promise, not a shortfall.** Every version deployed stays in the store, which is what makes a rollback a `publish_deploy` of an earlier version rather than a restore from a backup. The cost of that guarantee is the other half of it: a site cannot be wiped clean, only replaced. Taking one off the internet is a different act and it is available: `unpublish_site` stops serving it and keeps every version.
4. **It will not serve a bare apex domain everywhere.** A domain the customer already owns works on the paid tier — `claim_domain` returns the CNAME to add, or a link to approve it at their DNS provider where the provider supports that. `example.com` with no `www` works only if their DNS provider flattens CNAMEs or offers ALIAS records; if it does not, use `www.example.com` and have them redirect the bare name at their provider.
5. **It will not take a domain that is already behind another CDN.** Ownership is proven through the domain's DNS, and another CDN in front of it hides exactly what has to be read, so validation never completes. Say this before they try rather than after a week of waiting.


### Two bounds, and the sentence each one refuses with

Neither is a limit on what a site can be; both are limits on how a site is sent. Both are refusals — recorded, readable in `list_refusals`, and worded to say what to do next — so meet them before they are hit rather than retrying into them. ADR-0044.

- **A bundle is bounded at 4 MB as sent**, text and base64 counted together. Over it, `deploy_site` refuses with the size and the bound and changes nothing. Keep images small or serve big assets from elsewhere by URL.
- **One account may call each tool only so often per minute**: `deploy_site`, `request_upload` and `publish_deploy` 12, `start_build` 3, `create_database` 6, `apply_sql` 60, `claim_domain` 6, `set_database_url` 6, `reset_database_credential` 3, reads 240. Over it, the refusal says how many seconds to wait. If you are looping, the loop is the problem, not the platform.

**An `spa` bundle answers 200 for every path, including ones that do not
exist.** Measured 2026-09-02 against a deployed site. `spa` routing serves
`index.html` for anything the files do not match, which is what makes client-side
routing work and which also means a typo, a dead link and a missing asset all
come back **200 with the page shell**. A missing JavaScript module therefore
fails in the browser as a syntax error on `<` rather than as a 404, with the
network tab showing success — worth knowing before debugging one. The platform
does not decide this and cannot: it is what an SPA is. If the customer wants a
real 404, `static` routing gives one, and a file named **`404.html`** in the
bundle is what makes that 404 a page rather than a blank body.

**A domain takes a person and an unknown amount of time.** `claim_domain` returns one DNS record for the customer to add at their registrar — or, where their DNS provider supports Domain Connect, a link they open to approve that same record at the provider. No tool adds it without them, and no tool can watch them doing it. Hand them the record or the link, wait for them to say it is done, then call `domain_status` **once**. Do not poll in a loop: DNS takes as long as their provider's TTLs take, which is nothing either of us controls, and "not ready yet" for hours is normal. Two separate things must both be true before a domain serves — that it points here, and that a certificate exists for it — and `domain_status` reports them separately for a reason: a domain that resolves without a certificate fails in the browser with a security warning rather than a 404. **Do not tell a customer how long a certificate will take. It has not been measured.**

**One thing to be careful with.** `transfer_database` mints a one-time URL that moves a database out of the platform permanently. **Calling it is not a read — it marks the record as handed over immediately, before any person opens the link.** After that the platform reports the database as gone, and it cannot tell you whether anyone actually took it. Once the link *is* used, the platform cannot read that database, back it up, or get it back. Call it only when a person has asked to take their database, never to find out what would happen. `database_status` is the tool that answers questions.

## Confirm

When the setup works, print exactly this and stop:

```
✅ juudd connected — signed in as <account from hello>
   Twenty-two tools available. Deploys go to a subdomain with a Postgres database.
   Server functions are plain .js with no npm imports, and every table
   needs a row-level security policy.
```

If `hello` did not return an account, print what went wrong instead. Do not report success you did not observe. If you stopped for a restart or a sign-in, say which, what the person does next in one or two steps, and that pasting the same line afterwards finishes it.

## Resources

- The connector: `https://mcp.juudd.com/mcp`
- This file, hosted, and the one line a person pastes to send you here: <https://juudd.com/agent-setup>
- These rules, written for the person rather than for you: <https://juudd.com/docs>
- What the twenty-two tools each do: <https://juudd.com/tools>
- The Claude Code plugin: `https://juudd.com/marketplace.json`

**The paragraph that used to be here said the DNS was not cut over and to fall back to a Railway hostname. It is cut over.** Measured 2026-09-03: `https://mcp.juudd.com/healthz` and `https://juudd.com/` both answer 200. The fallback is gone from this document rather than left in as a harmless extra, because an alternative hostname in a setup file is one a reader can end up using by accident and then be connected to a name that is no longer the product's.

**The paragraph that used to be here said this file was not a page anyone could fetch. As of 2026-09-16 it is one: `https://juudd.com/agent-setup/prompt.md`, served as `text/markdown`.** It is not a copy. The landing site's build mounts `docs/agent-setup/` at `/agent-setup/` and refuses to finish if the served bytes differ from this file, so an edit here is the edit production serves on its next deploy, and there is no second file to forget. `https://juudd.com/agent-setup` is the page a person reads: one line to paste into you, which is how you most likely arrived. `https://juudd.com/docs` still carries the same rules written for the person rather than for you, and it has none of the instructions above about what to do yourself and what to print. ADR-0052.
