Developer KitBuild on OK Capsule's
public MCP.
Everything you need to recreate this Perfect Packs demo on top of OK Capsule's storefront MCP server. A zero-dependency TypeScript client, three runnable examples, and a drop-in prompt to bootstrap your own site in Lovable. No API keys — the endpoint is public.
Endpoint
storefront.okcapsule.app/mcp/perfect-packs
Auth
None — public
Protocol
MCP Streamable HTTP (JSON-RPC + SSE)
The three tools
The Perfect Packs reference site uses these three MCP tools. Call tools/list to see everything the server exposes.
okc_get_catalog()
Every supplement in the brand. Drives the /shop and pack-builder UIs.
okc_get_product_intelligence({ product_id })
Deep info on one supplement — benefits, dosage notes, suggested stacks.
okc_pack_builder_url({ pack_name, items[] })
Mints a pre-filled OK Capsule checkout URL for the proposed pack.
Try it in 60 seconds
Pure fetch — no install needed. Runs under Bun, Deno, or Node 22+.
git clone https://github.com/your-org/perfect-packs-mcp
cd perfect-packs-mcp/mcp-demo-kit
bun run examples/list-catalog.tsThe MCP client
Drop this file in your project. It handles the JSON-RPC handshake, session ID caching, and SSE frame parsing.
/**
* Minimal MCP client for OK Capsule's public storefront MCP.
*
* Endpoint: https://storefront.okcapsule.app/mcp/perfect-packs
* Auth: none — public, read-only-ish (pack-builder URL generation
* mutates nothing on OK Capsule's side until checkout).
*
* Speaks MCP Streamable HTTP (POST + JSON-RPC, SSE-framed responses).
* Zero dependencies — runs under bun, deno, or node >= 20.
*/
const MCP_URL = "https://storefront.okcapsule.app/mcp/perfect-packs";
let sessionId: string | null = null;
let requestId = 0;
let initPromise: Promise<void> | null = null;
async function rpc(method: string, params?: unknown, isNotification = false) {
const headers: Record<string, string> = {
"Content-Type": "application/json",
// REQUIRED by the MCP Streamable HTTP spec — servers reject calls
// missing either media type with HTTP 406.
Accept: "application/json, text/event-stream",
};
if (sessionId) headers["mcp-session-id"] = sessionId;
const body: Record<string, unknown> = { jsonrpc: "2.0", method };
if (!isNotification) body.id = ++requestId;
if (params !== undefined) body.params = params;
const res = await fetch(MCP_URL, {
method: "POST",
headers,
body: JSON.stringify(body),
});
// The server assigns a session ID on the first response; reuse it.
const sid = res.headers.get("mcp-session-id");
if (sid && !sessionId) sessionId = sid;
if (isNotification) return null;
if (!res.ok) {
throw new Error(`MCP ${method} failed (${res.status}): ${(await res.text()).slice(0, 300)}`);
}
// Responses come back as either a single JSON object or as an SSE
// stream with one "data: {...}" frame. Handle both.
const text = await res.text();
let payload: { result?: unknown; error?: { message: string } } | null = null;
for (const line of text.split("\n")) {
if (line.startsWith("data:")) {
payload = JSON.parse(line.slice(5).trim());
break;
}
}
if (!payload) payload = JSON.parse(text);
if (payload.error) throw new Error(`MCP ${method} error: ${payload.error.message}`);
return payload.result;
}
async function ensureInit() {
if (initPromise) return initPromise;
initPromise = (async () => {
await rpc("initialize", {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: { name: "okc-mcp-demo-kit", version: "1.0.0" },
});
// The "initialized" notification has no id and expects no response.
await rpc("notifications/initialized", undefined, true);
})();
return initPromise;
}
/** List every tool the MCP server exposes. */
export async function listTools() {
await ensureInit();
return rpc("tools/list");
}
/**
* Invoke a tool by name. Tool results come back as `content: [{type:"text",text}]`;
* the text is usually a JSON string, so we try to parse it for you.
*/
export async function callTool(name: string, args: Record<string, unknown> = {}) {
await ensureInit();
const result = (await rpc("tools/call", { name, arguments: args })) as {
content?: Array<{ text?: string }>;
};
const text = result?.content?.[0]?.text;
if (!text) return result;
try {
return JSON.parse(text);
} catch {
return text;
}
}
Example: list the catalog
/**
* List the full OK Capsule supplement catalog.
*
* Run: bun run examples/list-catalog.ts
* (or) node --experimental-strip-types examples/list-catalog.ts
*/
import { callTool } from "../mcp-client.ts";
const catalog = await callTool("okc_get_catalog");
const products: Array<{ id: string; product_name?: string; serving_size?: string }> =
Array.isArray(catalog) ? catalog : (catalog?.products ?? []);
console.log(`Found ${products.length} products`);
for (const p of products.slice(0, 25)) {
console.log(` ${p.id.padEnd(8)} ${p.product_name ?? "—"}`);
}
if (products.length > 25) console.log(` …and ${products.length - 25} more`);
Example: product detail
/**
* Fetch deep "product intelligence" for a single supplement:
* benefits, mechanism of action, suggested stacks, contraindications, etc.
*
* Run: bun run examples/product-detail.ts <product_id>
*/
import { callTool } from "../mcp-client.ts";
const productId = process.argv[2];
if (!productId) {
console.error("Usage: product-detail.ts <product_id>");
console.error("Tip: run list-catalog.ts first to find IDs.");
process.exit(1);
}
const detail = await callTool("okc_get_product_intelligence", { product_id: productId });
console.log(JSON.stringify(detail, null, 2));
Example: build a pack
/**
* Build a personalized 28-day pack and get a pre-filled checkout URL.
*
* Run: bun run examples/build-pack.ts
*/
import { callTool } from "../mcp-client.ts";
const result = await callTool("okc_pack_builder_url", {
pack_name: "Demo Pack",
items: [
{ product_id: "MAG-001", serving_size: 1, toa: "pm", cycle: "daily", duration: 28 },
{ product_id: "OMEGA-3", serving_size: 1, toa: "am", cycle: "daily", duration: 28 },
{ product_id: "VIT-D", serving_size: 1, toa: "am", cycle: "daily", duration: 28 },
],
});
const url =
(typeof result === "string" && result) ||
(result as { url?: string; checkout_url?: string; pack_builder_url?: string })?.url ||
(result as { checkout_url?: string })?.checkout_url ||
(result as { pack_builder_url?: string })?.pack_builder_url;
console.log("Pack checkout URL:");
console.log(url ?? result);
Build your own site in Lovable
Paste this prompt into a fresh Lovable project. It briefs the agent on the MCP endpoint, the three tools, and the pages to build.
# Lovable prompt — build your own MCP-powered supplement site
Paste this into a fresh [Lovable](https://lovable.dev) project to bootstrap a
Perfect Packs–style demo on top of OK Capsule's public MCP. No API keys
needed — the MCP endpoint is open.
---
Build a marketing + commerce site for personalized daily supplement packs.
The product catalog and pack-checkout URL come from OK Capsule's public
MCP server at `https://storefront.okcapsule.app/mcp/perfect-packs`
(JSON-RPC over HTTP, SSE-framed responses, no auth).
**Tools the MCP exposes that you should wire up:**
- `okc_get_catalog` — returns every supplement in the brand. Use it to
power a `/shop` browsing page and supplement detail dialogs.
- `okc_get_product_intelligence` (args: `{ product_id }`) — returns deep
info on one supplement: benefits, dosage notes, suggested stacks.
- `okc_pack_builder_url` (args: `{ pack_name, items: [{ product_id,
serving_size, toa: "am" | "pm", cycle: "daily", duration: 28 }] }`) —
returns a pre-filled OK Capsule checkout URL for the selected pack.
**Required pages:**
1. `/` — hero with an AI concierge chat widget that asks 3–4 questions
(goals, lifestyle, restrictions), then proposes a pack. The chat
should call `okc_get_catalog` for grounding and `okc_pack_builder_url`
to mint the final checkout link.
2. `/pack-builder` — manual UI: full catalog grid, add/remove items,
AM/PM toggle, live count, "Get my pack" → `okc_pack_builder_url`.
3. `/shop` — browse-only catalog with category filters.
4. `/how-it-works`, `/about`, `/faq`, `/disclaimer` — static content.
**Implementation rules:**
- Put MCP calls in a server-only module (`src/lib/okcapsule.server.ts`).
Never call the MCP directly from the browser — keep the session ID and
SSE parsing on the server.
- Use the JSON-RPC handshake: `initialize` → `notifications/initialized`
→ `tools/call`. Cache the session ID returned in the `mcp-session-id`
response header and reuse it for follow-up calls.
- Every outbound POST needs
`Accept: application/json, text/event-stream` — the MCP server returns
HTTP 406 without it.
- For the AI concierge, use Lovable AI with tool-calling. Expose the
three MCP tools to the model and let it orchestrate.
- Treat the OK Capsule catalog as the source of truth; don't hardcode
product lists.
**Tone:** clean, doctor-formulated, slightly editorial. Avoid medical
claims — describe ingredients with lifestyle/traditional-use language.
The reference implementation lives at
<https://perfect-packs-mcp.lovable.app> — feel free to study its
structure and copy patterns.
Full reference
The kit's README — protocol details, the catalog-vs-storefront gotcha, and an AI-agent wiring sketch.
# OK Capsule MCP — Demo Kit
A drop-in kit for building apps on top of [OK Capsule](https://okcapsule.com)'s
public storefront MCP server. Everything in this folder is what powers the
Perfect Packs reference site at <https://perfect-packs-mcp.lovable.app>.
- **Endpoint:** `https://storefront.okcapsule.app/mcp/perfect-packs`
- **Auth:** none — the endpoint is public.
- **Protocol:** MCP Streamable HTTP (JSON-RPC 2.0, SSE-framed responses).
## What's in here
| File | What it does |
|---|---|
| `mcp-client.ts` | Zero-dependency TypeScript MCP client. Handles session, SSE parsing, `tools/list`, `tools/call`. |
| `examples/list-catalog.ts` | Prints the full supplement catalog. |
| `examples/product-detail.ts` | Fetches deep info on one supplement. |
| `examples/build-pack.ts` | Builds a 28-day pack and prints a pre-filled OK Capsule checkout URL. |
| `lovable-prompt.md` | Paste this into [Lovable](https://lovable.dev) to bootstrap your own Perfect Packs–style site. |
## Try it in 60 seconds
```bash
git clone https://github.com/your-org/perfect-packs-mcp
cd perfect-packs-mcp/mcp-demo-kit
bun run examples/list-catalog.ts
```
(Or use `node --experimental-strip-types examples/list-catalog.ts` on
Node 22+. No `npm install` required — it's pure `fetch`.)
## The three tools used in the demo
- **`okc_get_catalog`** — every product in the brand. Returns an array of
`{ id, product_name, serving_size, description, ... }`.
- **`okc_get_product_intelligence`** — `{ product_id }` → benefits,
mechanism, suggested stacks, dosage guidance.
- **`okc_pack_builder_url`** — `{ pack_name, items: [{ product_id,
serving_size, toa: "am" | "pm", cycle: "daily", duration: 28 }] }` →
a pre-filled checkout URL on the OK Capsule storefront.
To see every tool the server actually exposes, call `tools/list`:
```ts
import { listTools } from "./mcp-client.ts";
console.log(await listTools());
```
## The JSON-RPC handshake
Every MCP client opens with the same dance, once per session:
```
POST https://storefront.okcapsule.app/mcp/perfect-packs
Content-Type: application/json
Accept: application/json, text/event-stream
{ "jsonrpc":"2.0", "id":1, "method":"initialize",
"params":{ "protocolVersion":"2025-06-18", "capabilities":{},
"clientInfo":{ "name":"my-app", "version":"1.0.0" } } }
← response includes `mcp-session-id: <uuid>` header
→ cache it and send it on every follow-up request
POST …same endpoint
mcp-session-id: <uuid>
{ "jsonrpc":"2.0", "method":"notifications/initialized" }
(notification — no `id`, no response expected)
POST …same endpoint
{ "jsonrpc":"2.0", "id":2, "method":"tools/call",
"params":{ "name":"okc_get_catalog", "arguments":{} } }
```
Two things to get right:
1. **`Accept: application/json, text/event-stream`** — both media types,
in one header. The server returns HTTP 406 without it.
2. **Responses are SSE-framed.** A reply usually comes back as
`data: {...json...}\n\n`. Strip the `data: ` prefix before parsing.
`mcp-client.ts` handles this — see the `rpc()` function.
## Gotcha: catalog vs. storefront-active subset
`okc_get_catalog` returns the full ingredient roster the brand has
*formulated*, but a given storefront only *sells* a curated subset of
those products. If you call `okc_pack_builder_url` with a product that's
in the catalog but not on the storefront, checkout will reject it with
"Product from url not found on this page".
For the Perfect Packs site we cross-check against the storefront's
widget endpoint and only ever recommend products that are actually
purchasable:
```
GET https://na1-prod.okcapsule.app/v2/pack-builders/<pack_builder_id>/widget/products
```
If you're building your own brand on OK Capsule, your storefront widget
URL is the source of truth for what's buyable today.
## Wiring it into an AI agent
The cleanest pattern: expose the MCP tools to your LLM via tool-calling,
and let the model orchestrate. A working system-prompt sketch:
```
You are a supplement concierge. You have these tools:
- okc_get_catalog(): list every available supplement.
- okc_get_product_intelligence(product_id): get deep info on one item.
- okc_pack_builder_url(pack_name, items[]): mint a checkout URL.
Ask the user about their goals, lifestyle, and any restrictions.
Propose a 4–6 item pack from the catalog. When they confirm,
call okc_pack_builder_url and return the link.
Use lifestyle/traditional-use language. Never make medical claims.
```
The reference implementation in `src/lib/okcapsule.server.ts` and
`src/lib/chat.functions.ts` of the demo site shows the full
TanStack Start + Lovable AI wiring.
## Reference site
<https://perfect-packs-mcp.lovable.app> — built end-to-end on this MCP.
Source: this repo.
---
© OK Capsule. The MCP server and supplement catalog are property of
OK Capsule. This demo kit is provided as a developer reference.
Reference implementation
This whole site is the reference. Browse the surfaces that wire the MCP into real product UI: