# Lupin MCP server

> Lupin is oleo's library of DTC landing-page sections. Its MCP server lets your agent search what every section says and pull any one in full.

Web page: https://tryoleo.com/sections/docs. Generated from the same source as the page, by tools/section-library/docs.mjs.

## Overview

Lupin collects the pages DTC brands send their Meta ads to and splits each one into its sections: hero, offer, reviews, FAQ, comparison and more. Every section is captured full height on a phone screen, with its brand, kind and page type.

The MCP server gives your agent the same library. Your agent can:

- search the text of every section with a regular expression, the way it would grep code;
- filter by section kind, brand, page type, or the role a line plays (heading, button, price, FAQ question);
- open one section in full: its text with roles, the captured HTML and links to its screenshots;
- use what it finds as a reference when it writes copy or builds a section for you.

Lupin is free. You sign in with your email and a 6-digit code. There is no password and no key to paste.

## Setup

Add Lupin to your client, then sign in once.

### Claude Code

Add the server:

```bash
claude mcp add --transport http lupin https://tryoleo.com/mcp
```

Then sign in. Run `/mcp` inside Claude Code, pick **lupin** and follow the browser steps. From a shell, run:

```bash
claude mcp login lupin
```

The command above adds Lupin to the current project only. Add `--scope user` to load it in every project:

```bash
claude mcp add --transport http --scope user lupin https://tryoleo.com/mcp
```

### Codex

Add the server, then sign in:

```bash
codex mcp add lupin --url https://tryoleo.com/mcp
codex mcp login lupin
```

Or add it to `~/.codex/config.toml` yourself, then run `codex mcp login lupin`:

```toml
[mcp_servers.lupin]
url = "https://tryoleo.com/mcp"
```

The Codex CLI, the IDE extension and the ChatGPT desktop app share this configuration.

Lupin also works as a custom connector in Claude (Customize → Connectors → Add custom connector) and ChatGPT (add a custom MCP server; plan support varies). Use the URL `https://tryoleo.com/mcp`.

When you sign in, your browser opens Lupin's sign-in page. Enter your email, then the 6-digit code we send. Approve the client once and you're connected.

## Tools

Lupin has two tools. Both only read the library.

### `grep_sections`

Search the text of every section with a regular expression. Each result line is one piece of text, labelled with its role and the section it comes from.

The full description your agent reads:

```text
Search the text of every DTC landing-page section in Lupin, the oleo section library (the pages DTC brands send their Meta ads to, every section of each), with a regular expression, the way you would grep code. Each line is one text unit of a section, labelled by the role its HTML gives it: heading, subheading, body, button, link, price, compare-at-price (struck), badge, caption, label, list-item, summary (accordion question), option, quote, table-header, table-cell, image-alt, placeholder, or text (no role in the code). button is a real button or a link styled as one (a CTA); option is one choice among several (a radio and its label, a select option, a swatch, a variant or plan tile). hidden lines are in the page but not on screen (closed accordion answers, other slides, other tabs). Every line carries its block id ([B3]): the nearest structural element around it in the code; block true prints each match with its whole block. A design on several pages shows once per distinct line, and each line lists every other page copy that holds it. Each section gives the section page, the phone screenshot URL and the live page. format "compact" gives one line per section. Use get_section for the full role tree, the HTML and the screenshot URLs. Each call counts against the account's 100 free calls; prefer one well-filtered call with a higher limit to many small ones. Patterns are capped at 500 characters. Examples: pattern "save \\d+%|\\d+% off" role "button"; pattern "money.?back|guarantee" format "compact"; pattern "subscribe" kind "offer" block true.
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `pattern` | string, up to 500 characters | required | JavaScript regular expression matched against each line's text; case-insensitive unless caseSensitive |
| `caseSensitive` | boolean | `false` | Match case (default false) |
| `kind` | string or string[] | none | Section kind(s): hero, offer, reviews, faq, usp, comparison, how-it-works, product-showcase, creators, press, guarantee, ingredients, story, other |
| `brand` | string or string[] | none | Brand id(s) or name(s) |
| `pageType` | string or string[] | none | Page type(s): Landing page, Product page, Homepage, Collection, Listicle, Advertorial, Other ... |
| `role` | string or string[] | none | Role(s), as listed above, e.g. "button" or ["price","compare-at-price"] |
| `hidden` | boolean | none | true: only text hidden on the page; false: only visible text; omit for both |
| `context` | integer, 0 to 20 | none | Role-tree lines to show before and after each match (like grep -C) |
| `block` | boolean | none | Print each match's whole block (the card, form or Shopify block it sits in), the match marked > |
| `format` | "full" or "compact" | `full` | full (default): designs, sections, blocks, page copies; compact: one line per section |
| `limit` | integer, 1 to 200 | `30` | Lines per call (default 30) |
| `offset` | integer, 0 or more | `0` | Lines to skip, for the next page (default 0) |

### `get_section`

Get one section in full: its facts, its text with roles, the captured HTML and its screenshot links.

The full description your agent reads:

```text
One section of the library by its sectionUuid (or its tryoleo.com/sections/s/<uuid> URL): brand, kind, page type, live page, its other page copies, its blocks, the full role tree (every text unit with its code role and block, hidden ones marked), the captured outerHTML (each element marked data-oleo-v="1" visible or "0" hidden; at most 150000 bytes, truncated with a marker), and the screenshots as https URLs: the full-height section image and the first 390x844 phone screen.
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `id` | string, up to 200 characters | required | sectionUuid from grep_sections, or the section page URL |
| `htmlLimit` | integer, 0 to 150000 | `150000` | Bytes of HTML to return (default and maximum 150000; 0 for none) |
| `image` | boolean or "section" or "screen" | `"section" or true` | "section" or true (default): link the full-height section image and the phone screen; "screen": only the phone screen; false: no image links |

## Example prompts

Ask your agent in plain words. It picks the tool and the filters.

- "Find FAQ sections that answer shipping objections and show me the three strongest."
- "Get the offer section from Huel's page and rebuild it as a Shopify section."
- "Which brands show a struck-through compare-at price on their offer? List the sections with their prices."
- "Collect every money-back guarantee line on landing pages and group them by how long the guarantee lasts."
- "Show me hero sections whose button says Shop now or Get started, with their phone screenshots."

## Rate limits

Each account gets **100 free tool calls in total**, then Lupin Pro for unlimited calls. Every account can make **60 calls a minute**.

| Limit | Window | Resets |
|---|---|---|
| 100 calls | the life of the account | never: Lupin Pro has no total limit |
| 60 calls | the clock minute | at the start of the next minute |

Every `tools/call` counts, including one that returns an error such as a bad pattern. Listing the tools does not count. A call refused by a limit is not added to your total.

When you have used your free calls, the call returns a tool error, and your account joins the Lupin Pro waitlist:

```text
You've used your 100 free Lupin calls. Lupin Pro (£18 / $20 a month, unlimited) opens soon. You're on the waitlist, and we'll email you when it opens.
```

Past the per-minute limit, the error says when it resets:

```text
Rate limit reached: 60 Lupin tool calls per minute. Retry after 2026-11-01T15:21:00Z (in 30s).
```

To save calls, ask for one well-filtered search with a higher `limit` rather than many small ones.

## Endpoint and transport

|  |  |
|---|---|
| URL | `https://tryoleo.com/mcp` |
| Transport | Streamable HTTP, JSON-RPC 2.0 sent by POST |
| Sessions | none: each request stands alone and gets a JSON response |
| Client registration | dynamic client registration, so there is no app to create |

The server is stateless. It has no event stream, so `GET` and `DELETE` answer `405`.

## Authentication

Lupin uses OAuth 2.1 through your client: the authorization code flow with PKCE. Your client registers itself and opens the browser. You sign in with an emailed 6-digit code. The code works for 10 minutes and allows 5 tries.

A request without a valid token gets `401` with this header, which tells the client where to start:

```text
WWW-Authenticate: Bearer resource_metadata="https://tryoleo.com/.well-known/oauth-protected-resource"
```

| Document | URL |
|---|---|
| Protected resource metadata | `https://tryoleo.com/.well-known/oauth-protected-resource` |
| Authorization server metadata | `https://tryoleo.com/.well-known/oauth-authorization-server` |
| Issuer | `https://tryoleo.com/api/auth` |
| Signing keys (JWKS) | `https://tryoleo.com/api/auth/jwks` |

Access tokens are signed JWTs that last one hour. Your client renews them with a refresh token, which lasts 30 days.

## Security

- **Read-only tools.** Both tools only read Lupin's own copy of the library. They are marked `readOnlyHint` so clients don't treat them as write actions.
- **Short-lived, scoped tokens.** Each access token lasts one hour and is bound to `https://tryoleo.com/mcp`. The server checks its signature, issuer, audience and expiry on every request, and accepts no token issued for anything else.
- **What we store.** Your email, whether you ticked "Email me Lupin updates", the clients you connect and the tokens we issue them, and your usage counts. Sign-in codes are stored hashed. Per-minute counts are deleted within two days; your total stays as long as your account.
- **What we log.** One line per tool call: your user id, the tool, the result size, the status and the time it took. Never your token or your email.
- **Per-account limits.** 100 free calls in total and 60 a minute, as above.

Read the full [privacy policy](https://tryoleo.com/privacy).

## Troubleshooting

### 401, "needs authentication" or no Lupin tools

Your client is not signed in, or its sign-in expired. Sign in again:

```bash
claude mcp login lupin
```

```bash
codex mcp login lupin
```

In Claude Code you can also run `/mcp` and pick **lupin**. Headless runs such as `claude -p` can't open the browser, so sign in from an interactive session first. Don't add an `Authorization` header to the server's config: it stops the sign-in.

### "You've used your 100 free Lupin calls"

Your account has made its 100 free calls and is on the waitlist for Lupin Pro, which has no total limit. We'll email you when it opens.

### "Rate limit reached"

The per-minute limit resets at the start of the next minute. The error says when.

### "Lupin is temporarily unavailable"

The server could not reach its usage store. Try again in 30 seconds.

### Remove and add again

Adding a server with a name that already exists fails, so remove Lupin first.

```bash
claude mcp remove lupin
claude mcp add --transport http lupin https://tryoleo.com/mcp
```

```bash
codex mcp remove lupin
codex mcp add lupin --url https://tryoleo.com/mcp
```

Then sign in again.
