diff --git a/src/pages/docs/features/_meta.ts b/src/pages/docs/features/_meta.ts
index c2bb629..e3625e8 100644
--- a/src/pages/docs/features/_meta.ts
+++ b/src/pages/docs/features/_meta.ts
@@ -57,6 +57,9 @@ export default {
"mcp-integration": {
"title": "MCP Integration"
},
+ "mcp-server": {
+ "title": "MCP Server"
+ },
"cli": {
"title": "CLI (lfc)"
}
diff --git a/src/pages/docs/features/mcp-server.mdx b/src/pages/docs/features/mcp-server.mdx
new file mode 100644
index 0000000..de5ccb7
--- /dev/null
+++ b/src/pages/docs/features/mcp-server.mdx
@@ -0,0 +1,293 @@
+---
+title: MCP Server
+tags:
+ - ai
+ - mcp
+ - tools
+ - claude
+ - cursor
+ - vscode
+ - codex
+---
+
+import { Callout, Steps } from "nextra/components";
+
+Lifecycle ships a built-in **MCP server** (Model Context Protocol — a standard way for AI
+assistants to call tools), so assistants like Claude Code, Cursor, VS Code, and Codex can look at
+your preview environments for you. Once connected, you can ask your AI tool things like:
+
+- _"Is the environment for my PR up? What's the frontend URL?"_
+- _"Why did the backend build fail in `snowy-field-978044`? Show me the logs."_
+- _"List my environments and tell me which ones errored."_
+
+The assistant answers by calling Lifecycle directly — no copy-pasting UUIDs, dashboards, or log
+snippets into the chat.
+
+
+ Looking for the opposite direction — connecting **external** MCP servers to
+ Lifecycle's built-in [AI Agent](/docs/features/ai-agent)? See [MCP
+ Integration](/docs/features/mcp-integration).
+
+
+## Connect your AI tool
+
+Your Lifecycle MCP URL is your Lifecycle app host plus `/mcp` — for example
+`https://app.lifecycle.example.com/mcp`. If your admin has enabled dynamic client registration,
+that URL is all you need — registration and login happen automatically. Otherwise (the default),
+you also need an OAuth client ID from your admin — see
+[Without dynamic client registration](#without-dynamic-client-registration).
+
+
+ Two prerequisites: your Lifecycle admin must have the MCP server **enabled**,
+ and you need network access to your Lifecycle app host (e.g. VPN) — the same
+ reachability as the Lifecycle UI.
+
+
+The first time you connect, your browser opens your organization's normal single sign-on page —
+the same login as the Lifecycle UI and `lfc` CLI — followed by a one-time consent screen. That's
+it. When your session eventually expires, just run the client's login step again.
+
+### Claude Code
+
+```bash
+claude mcp add --transport http lifecycle https://app.lifecycle.example.com/mcp
+claude mcp login lifecycle
+```
+
+Or run `/mcp` inside a session and pick **lifecycle → Authenticate**.
+
+### Cursor
+
+Add the server to `~/.cursor/mcp.json` (or per-project `.cursor/mcp.json`):
+
+```json filename="~/.cursor/mcp.json"
+{
+ "mcpServers": {
+ "lifecycle": { "url": "https://app.lifecycle.example.com/mcp" }
+ }
+}
+```
+
+Cursor shows a **Needs login** prompt next to the server in _Settings → MCP_; clicking it opens
+the browser sign-in.
+
+### VS Code
+
+Add the server to `.vscode/mcp.json` in your workspace (or your user-level `mcp.json`):
+
+```json filename=".vscode/mcp.json"
+{
+ "servers": {
+ "lifecycle": {
+ "type": "http",
+ "url": "https://app.lifecycle.example.com/mcp"
+ }
+ }
+}
+```
+
+VS Code prompts you to trust and authenticate the server the first time Copilot Chat uses it.
+
+### Codex CLI
+
+Add the server to `~/.codex/config.toml`, then log in:
+
+```toml filename="~/.codex/config.toml"
+[mcp_servers.lifecycle]
+url = "https://app.lifecycle.example.com/mcp"
+```
+
+```bash
+codex mcp login lifecycle
+```
+
+
+ Try it with a prompt like _"using the lifecycle tools, list my environments"_.
+ Every tool call runs under your identity — the same login as the Lifecycle UI
+ — but note that this scopes filters, not visibility; see the note under [What
+ you can do](#what-you-can-do).
+
+
+## What you can do
+
+All v1 tools are **read-only**: your assistant can look things up, but it cannot deploy, redeploy,
+delete, or change environment variables from here — use the Lifecycle UI or the `lfc` CLI for
+those.
+
+
+ Like the REST API and UI, any authenticated user can read any build, service,
+ log, or site — `myEnvironmentsOnly`/`mineOnly` are convenience filters, not
+ access controls. If a preview environment's logs may contain sensitive output,
+ treat them as visible to all authenticated Lifecycle users.
+
+
+| Tool | What it answers |
+| --------------- | -------------------------------------------------------------------------------------------------- |
+| `list_builds` | "What environments exist?" — searchable, pageable, with a _my environments only_ filter |
+| `get_build` | "What's the status of environment X?" — services, URLs, branch, and PR details for one build |
+| `list_services` | "Which services are in this environment, and where are they running?" — status, branch, image, URL |
+| `get_job_logs` | "Why did the build/deploy fail?" — build-job or deploy-job logs, live from the cluster or archived |
+| `list_sites` | "What static sites are published?" |
+| `get_site` | "Where is site X and when does it expire?" |
+
+It also exposes one MCP **resource**, `lifecycle://builds/{uuid}`, which returns a build's detail
+document as JSON for clients that support attaching resources.
+
+### Tool reference
+
+| Tool | Parameters |
+| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `list_builds` | `search` (string, optional) · `myEnvironmentsOnly` (boolean, optional) · `page` (int, optional) · `limit` (int, optional, default 25, max 100) |
+| `get_build` | `uuid` (string, required) |
+| `list_services` | `uuid` (string, required) |
+| `get_job_logs` | `uuid` (string, required) · `service` (string, required) · `jobType` (`build` \| `deploy`, required) · `jobName` (string, optional — defaults to the most recent job) · `tailLines` (int, optional, default 200, max 2000) |
+| `list_sites` | `mineOnly` (boolean, optional) · `page` (int, optional) · `limit` (int, optional, default 25, max 100) |
+| `get_site` | `siteId` (string, required) |
+
+## How it works
+
+A one-page tour for the curious:
+
+1. **One endpoint on the web service.** The MCP server is part of the Lifecycle web deployment,
+ served over MCP's Streamable HTTP transport at `/mcp`. There is nothing extra to run — it reads
+ the same data the UI and API do, and it's stateless, so it scales with your web replicas.
+
+2. **Login is your normal SSO.** Lifecycle's Keycloak realm acts as the OAuth 2.1 authorization
+ server. When a tool connects for the first time it discovers the auth settings automatically
+ (via standard OAuth metadata), identifies itself to Keycloak — by registering itself when your
+ admin has enabled dynamic client registration, or with the client ID your admin pre-registered —
+ and sends you through the same browser single sign-on used by the UI and the `lfc` CLI. A
+ one-time consent screen shows what the tool is asking for.
+
+3. **Tokens only work here.** Access tokens issued for the MCP server are bound to its exact URL
+ (the OAuth _audience_). A CLI or UI token is rejected at `/mcp`, and an MCP token is rejected
+ by the REST API — one leaked token never unlocks the other surface.
+
+4. **Everything runs under your identity.** Each tool call executes under the identity in your
+ token, so filters like _my environments only_ use your account — though, as in the UI, any
+ authenticated user can read any environment (see [What you can do](#what-you-can-do)). Tool
+ output never includes service environment variables, which can contain secrets.
+
+## Setup (admins)
+
+Enabling the MCP server for your Lifecycle deployment is two switches — one on the app, one on
+Keycloak.
+
+
+### Enable the endpoint on the web service
+
+Set two environment variables on the Lifecycle web deployment:
+
+```yaml filename="values.yaml (lifecycle chart, web component)"
+components:
+ web:
+ deployment:
+ extraEnv:
+ - name: MCP_SERVER_ENABLED
+ value: "true"
+ - name: MCP_RESOURCE_URL
+ value: "https://app.lifecycle.example.com/mcp"
+```
+
+`MCP_RESOURCE_URL` is the canonical URL clients will use — access tokens are audience-bound to
+this exact value, so it **must match** the `resourceUrl` you give Keycloak below, byte for byte
+(scheme, host, port, path). The endpoint is served only by the web process (`LIFECYCLE_MODE`
+`web`/`all`) and is absent unless `MCP_SERVER_ENABLED` is `true`.
+
+### Configure the Keycloak realm
+
+The realm needs an `mcp` client scope (with audience and identity mappers) and, optionally,
+anonymous dynamic client registration so tools can self-register. The `lifecycle-keycloak` chart
+does this for you with an idempotent post-install/post-upgrade job:
+
+```yaml filename="values.yaml (lifecycle-keycloak chart)"
+mcp:
+ enabled: true
+ resourceUrl: "https://app.lifecycle.example.com/mcp"
+ # Only when Keycloak is NOT reachable from the public internet — see the warning below.
+ dcr:
+ enabled: true
+ # Alternate URLs for the SAME deployment (e.g. in-cluster access, host dev server):
+ # extraAudiences:
+ # - "http://lifecycle-web.lifecycle-app.svc.cluster.local/mcp"
+ # - "http://localhost:8000/mcp"
+```
+
+The Job is idempotent and runs on both fresh installs and existing realms, so a Helm upgrade with
+these values is all that's needed — no separate script to run by hand.
+
+
+ All URLs listed (`resourceUrl` + `extraAudiences`) share a single token trust
+ boundary — an access token issued for one is accepted by all of them. Use
+ `extraAudiences` only for alternate URLs of the **same** deployment; do not
+ use it to share one realm across production and staging.
+
+
+### Verify
+
+```bash
+curl https://app.lifecycle.example.com/.well-known/oauth-protected-resource/mcp
+```
+
+should return the server's OAuth metadata, and connecting any MCP client to
+`https://app.lifecycle.example.com/mcp` should walk you through browser login.
+
+
+
+
+ **Anonymous dynamic client registration is off by default and one-way to
+ enable.** It lets anyone who can reach your Keycloak host register an OAuth
+ client (registering grants no access by itself — a real SSO login is still
+ required for tokens), and turning it on **deletes** the realm's anonymous
+ Trusted Hosts policy, which cannot be undone by disabling the setting later.
+ Only enable it (`mcp.dcr.enabled: true` in the chart, or
+ `--enable-anonymous-dcr` with the script) when Keycloak is **not** reachable
+ from the public internet. Otherwise leave it off and pre-register a client for
+ your users, as described below. Back up the realm first if you may want to
+ revert.
+
+
+### Without dynamic client registration
+
+When dynamic client registration stays disabled (the default), pre-register one OAuth client in
+the Lifecycle Keycloak realm and distribute its client ID to your users:
+
+1. In the realm, create a **public** client (no client secret) with the **Standard flow** enabled
+ and PKCE enforced (set _Proof Key for Code Exchange Code Challenge Method_ to `S256` in the
+ client's advanced settings).
+2. Turn on **Consent required** in the client's settings, so users get the same one-time consent
+ screen that dynamically registered clients show.
+3. Add the redirect URIs your users' MCP clients need — CLI tools typically use a localhost
+ callback such as `http://localhost:8080/callback`, while some editors use fixed HTTPS or
+ app-scheme callbacks. Check each client's documentation for its exact callback URL.
+4. Assign the `mcp` client scope to the client (as an optional client scope).
+5. Hand the client ID to your users.
+
+Support for pre-registered client IDs varies by MCP client, so check your client's documentation
+for its current state. At the time of writing:
+
+- **Claude Code** supports it — pass the ID and pin the callback port when adding the server, and
+ register the matching `http://localhost:8080/callback` redirect URI in Keycloak:
+
+ ```bash
+ claude mcp add --transport http --client-id --callback-port 8080 \
+ lifecycle https://app.lifecycle.example.com/mcp
+ ```
+
+- **Cursor** supports it — add an `auth` object next to the server's `url` in `mcp.json` with
+ `"CLIENT_ID": ""`, and register Cursor's fixed callbacks as redirect URIs:
+ `https://www.cursor.com/agents/mcp/oauth/callback` (web/cloud agents) and
+ `cursor://anysphere.cursor-mcp/oauth/callback` (desktop app).
+
+- **VS Code** and **Codex CLI** rely on dynamic client registration and do not currently document
+ a way to supply a pre-registered client ID — consult their documentation for updates.
+
+## Troubleshooting
+
+| Symptom | Likely cause |
+| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Client says the server needs authentication and nothing happens | Run the client's login step (`claude mcp login …`, `codex mcp login …`, or the login prompt in Cursor/VS Code settings) |
+| Browser login loops or shows `invalid_scope` | The Keycloak realm is missing the `mcp` client scope configuration — re-run the setup job or script |
+| `401` with a valid login | The token audience doesn't match `MCP_RESOURCE_URL` — make sure the URL configured in your client matches it exactly |
+| Tools list is empty or calls fail after login | The endpoint may be disabled (`MCP_SERVER_ENABLED`) on the web deployment |
+| Registration rejected | Dynamic client registration is disabled — the expected default. Use a pre-registered client ID (see [Without dynamic client registration](#without-dynamic-client-registration)) or ask your Lifecycle admin |