Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/pages/docs/features/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export default {
"mcp-integration": {
"title": "MCP Integration"
},
"mcp-server": {
"title": "MCP Server"
},
"cli": {
"title": "CLI (lfc)"
}
Expand Down
293 changes: 293 additions & 0 deletions src/pages/docs/features/mcp-server.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Callout type="info">
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).
</Callout>

## 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).

<Callout type="info">
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.
</Callout>

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
```

<Callout type="tip">
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).
</Callout>

## 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.

<Callout type="warning">
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.
</Callout>

| 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.

<Steps>
### 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.

<Callout type="warning">
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.
</Callout>

### 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.

</Steps>

<Callout type="error" emoji="⚠️">
**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.
</Callout>

### 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 <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": "<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 |
Loading