---
title: The dev loop
description: Iterate against a local ggui runtime, then point the same agent at hosted ggui by swapping two environment variables — including the endpoint-shape trap that bites on the first deploy.
---

Your agent talks to ggui over MCP and nothing else. That single fact is what makes the
local-to-hosted move small: the tools, the contracts, and the system prompt are identical
on both sides, so promoting an agent from your laptop to `mcp.ggui.ai` is a change of URL
and bearer, not a change of code.

This page is the loop that follows from that: iterate locally where restarts are instant
and generation runs on your own provider key, then flip the endpoint when you need a real
user in front of the render.

## The two local servers

The CLI ships two local runtimes and they are not interchangeable.

| | [`ggui dev`](/cli/dev/) | [`ggui serve`](/cli/serve/) |
| --- | --- | --- |
| Default bind | `127.0.0.1:6780` | `127.0.0.1:6781` |
| Serves MCP | No | Yes, at `/mcp` |
| For | Iterating on blueprints and gadgets in the dev hub | Running the protocol the way production runs it |

`ggui dev` is a blueprint registry plus a dev hub — it indexes the `ggui.ui.json` manifests
declared in `ggui.json#blueprints.include`, previews them at `/hub/preview?ui=<id>`, and with
`--agent <entry>` supervises your agent process in the same shell. It deliberately does not
run an MCP server.

`ggui serve` is the one your agent connects to. It boots `@ggui-ai/mcp-server` and, by
default, the agent declared in `ggui.json#agent.entry`. Both can run side by side; the ports
do not collide.

```bash
ggui serve --dev-allow-all   # MCP at http://127.0.0.1:6781/mcp, any bearer accepted
ggui dev                     # blueprint hub at http://127.0.0.1:6780/hub
```

`--dev-allow-all` accepts any bearer (or none) as a `builder` identity. It is a loopback
convenience and the boot banner says so — never put it on a public URL. Without it, `ggui serve`
requires a pair-minted bearer.

## The endpoint shape differs, and it is the first thing that breaks

The two deployments do not spell their MCP endpoint the same way.

- **Local `ggui serve`** serves MCP at the `/mcp` sub-path: `http://127.0.0.1:6781/mcp`.
- **Hosted ggui** serves each app at its bare per-app path: `https://mcp.ggui.ai/apps/<appId>`.

The `/mcp` suffix is a local-`ggui serve` convention only. Append it to the hosted URL and the
per-app pod returns 404, which surfaces as an agent that discovers zero ggui tools rather than
as an obvious connection error. Carry the suffix over from your local config and this is the
symptom you will see.

## Swap the deployment with environment variables

The reference samples read the endpoint and the bearer from the environment, so the two
deployments are two `.env.local` files rather than two code paths.

<Tabs syncKey="deployment">
<TabItem label="Hosted">

```bash
# .env.local — hosted ggui
GGUI_MCP_URL=https://mcp.ggui.ai/apps/aB3kP9xY
GGUI_MCP_BEARER=ggui_user_xxxxxxxx
ANTHROPIC_API_KEY=sk-ant-...
```

Both values come out of [the ggui console](/hosted/console/) or the CLI: `ggui login`, then
`ggui keys create --name my-agent` mints the `ggui_user_*` connector key. The app ID is an opaque
8-character string with no prefix — the app's page shows the whole endpoint, `mcp.ggui.ai/apps/<appId>`,
ready to copy.

</TabItem>
<TabItem label="Self-hosted">

```bash
# .env.local — local ggui serve
GGUI_MCP_URL=http://127.0.0.1:6781/mcp
GGUI_MCP_BEARER=dev
ANTHROPIC_API_KEY=sk-ant-...
```

`dev` is accepted because `ggui serve --dev-allow-all` accepts any bearer. Under the default
strict-auth posture, use a pair-minted bearer instead — see [Pair a client app](/self-hosted/pairing/).

</TabItem>
</Tabs>

Nothing else moves. The agent wiring reads both values and hands them to the MCP client:

```typescript
const mcpServers = {
  ggui: { url: process.env.GGUI_MCP_URL ?? "http://localhost:6781/mcp" },
};
```

That is the shape used by every sample in
[`samples/agents/`](https://github.com/ggui-ai/ggui/tree/main/samples/agents) — Claude Agent SDK,
OpenAI Agents SDK, and Google ADK all read the same two variables.
`@ggui-ai/agent-server` resolves the bearer as `opts.bearer ?? process.env.GGUI_MCP_BEARER ?? "dev"`
and sends it as `Authorization: Bearer <token>` on every MCP request. Wiring the SDK by hand
instead? [The Claude agent example](/examples/claude-agent/) shows the explicit `headers` form.

## Let the CLI write the hosted half

`ggui deploy` does the whole hosted-side provisioning in one idempotent pass: sign in, create
the cloud app and write its ID into `ggui.json`, mint a connector key into `.env.local` as
`GGUI_MCP_BEARER`, upload your local blueprint pool, push declared gadgets and `publicEnv`, and
finally write `GGUI_MCP_URL`. Re-run it after a partial failure and it resumes from where it
stopped.

```bash
ggui deploy
```

It writes the endpoint in the correct bare `/apps/<appId>` shape, which is the practical reason
to prefer it over hand-editing the URL. An existing `GGUI_MCP_URL` is never clobbered — the first
deploy writes it, later ones leave your override alone. Add `--push-keys` to also push your
provider key to the app so `keySource=own` renders run on your own model account.

## What actually changes when you cross over

**Who pays for generation.** Local `ggui serve` calls your provider directly with the key in
your environment — no ggui credits involved. Hosted renders draw down your credit balance
unless the app runs on your own provider key, in which case ggui skips the charge entirely.
See [Credits, billing & BYOK](/hosted/billing/).

**Auth posture.** `--dev-allow-all` is a loopback-only shortcut. Hosted requires a real
`ggui_user_*` connector key per app, revocable from the CLI.

**The blueprint pool.** Blueprints you matured locally are worth carrying over — a hosted app
that already has them matches from cache instead of paying to synthesize the same UI again.
`ggui push` compiles and uploads the local pool to a cloud app (`ggui deploy` runs it as one of
its steps); `ggui export-pool` writes the pool to a directory that another `ggui serve` can load
with `--seed-pool <dir>`.

**Rate limits.** Hosted enforces per-key limits that a loopback server does not — see
[Rate limits](/hosted/rate-limits/).

What does not change: the `ggui_*` tool surface, the handshake-render-consume sequence,
`GGUI_AGENT_SYSTEM_PROMPT`, your contracts, and your component code. That is the portability
claim, and it is testable — see [Portability](/hosted/portability/).

## A loop that works

1. `ggui serve --dev-allow-all` in one shell, your agent pointed at `http://127.0.0.1:6781/mcp`.
2. `ggui dev` in another when you are shaping blueprints; edit `ggui.ui.json`, reload the hub,
   the registry re-indexes on every load.
3. `ggui dev --no-serve` in CI — it loads and validates every manifest and exits non-zero on a
   malformed one, without binding a socket.
4. When you need a real user in front of the render, `ggui deploy`, then restart the agent with
   the hosted `.env.local`.
5. Need a public URL without leaving the local runtime? Run a tunnel beside it
   (`cloudflared tunnel --url http://localhost:6781`) and point the client at
   `https://<tunnel>/mcp`.

## See also

- [`ggui dev`](/cli/dev/) — every flag, the dev-hub route table, agent supervision
- [`ggui serve`](/cli/serve/) — auth postures, persistence bundle, reference deploys
- [Build an agent on hosted ggui](/quickstart/hosted-agent/) — the hosted path end to end
- [Self-hosted quickstart](/quickstart/self-hosted/) — the self-hosted bootstrap
- [Connector keys](/hosted/connector-keys/) — what a `ggui_user_*` key authorizes