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 |