Skip to content

Commit db5a7c1

Browse files
committed
feat(website): add the MCP landing page
1 parent d5af45a commit db5a7c1

1 file changed

Lines changed: 330 additions & 0 deletions

File tree

src/pages/mcp.astro

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
---
2+
// MCP landing page. Served on its own host (mcp.rivet.dev) via a redirect, so
3+
// it uses the standalone floating header rather than a product bar.
4+
import * as shiki from 'shiki';
5+
import codeTheme from '@/lib/textmate-code-theme';
6+
import MarketingLayout from '@/layouts/MarketingLayout.astro';
7+
import { InkPanel, InkChip } from '@/components/marketing/editorial/InkPanel';
8+
import { Icon, faClaude, faOpenai, faCursor, faGemini, faLaptopCode, faVscode } from '@rivet-gg/icons';
9+
import { RedesignedCTA } from '@/components/marketing/sections/RedesignedCTA';
10+
import {
11+
PRODUCT_HERO_CTA_ROW_CLASS,
12+
PRODUCT_HERO_H1_CLASS,
13+
PRODUCT_HERO_PRIMARY_BUTTON_CLASS,
14+
PRODUCT_HERO_SECONDARY_BUTTON_CLASS,
15+
PRODUCT_HERO_SECTION_CLASS,
16+
SECTION_H2_CLASS,
17+
SUBTITLE_CLASS,
18+
} from '@/components/marketing/typography';
19+
20+
const ENDPOINT = 'https://mcp.rivet.dev/mcp';
21+
22+
const clients = [
23+
{
24+
name: 'Claude Code',
25+
icon: faClaude,
26+
command: `claude mcp add --transport http rivet ${ENDPOINT}`,
27+
},
28+
{ name: 'Codex', icon: faOpenai, command: `codex mcp add rivet --url ${ENDPOINT}` },
29+
{ name: 'Cursor', icon: faCursor, config: 'remote' },
30+
{
31+
name: 'Gemini CLI',
32+
icon: faGemini,
33+
command: `gemini mcp add --transport http rivet ${ENDPOINT}`,
34+
},
35+
{
36+
name: 'VS Code',
37+
icon: faVscode,
38+
command: `code --add-mcp '{"name":"rivet","type":"http","url":"${ENDPOINT}"}'`,
39+
},
40+
{
41+
name: 'Local',
42+
icon: faLaptopCode,
43+
config: 'local',
44+
note: 'Talk to the Rivet already running on your machine. No URL and no sign-in.',
45+
},
46+
];
47+
48+
const scopes = [
49+
{ url: ENDPOINT, label: 'Everything your account can reach' },
50+
{ url: `${ENDPOINT}?organization=ORG`, label: 'One organization' },
51+
{ url: `${ENDPOINT}?organization=ORG&project=PROJECT`, label: 'One project' },
52+
{
53+
url: `${ENDPOINT}?organization=ORG&project=PROJECT&namespace=NS`,
54+
label: 'One namespace',
55+
},
56+
];
57+
58+
const REMOTE_CONFIG = `{
59+
"mcpServers": {
60+
"rivet": {
61+
"url": "${ENDPOINT}"
62+
}
63+
}
64+
}`;
65+
66+
const CODE_MODE_EXAMPLE = `// "Which chat rooms are still awake, and what has Acme been saying?"
67+
const { actors } = await rivet.actors.list({ name: "chat-room" });
68+
const awake = actors.filter((room) => room.status === "running");
69+
70+
const history = await rivet.actor.action({
71+
actor: { name: "chat-room", key: ["acme"] },
72+
name: "getHistory",
73+
});
74+
75+
return { awake: awake.length, history };`;
76+
77+
const LOCAL_CONFIG = `{
78+
"mcpServers": {
79+
"rivet": {
80+
"command": "npx",
81+
"args": ["-y", "@rivet-dev/mcp", "--target", "local"]
82+
}
83+
}
84+
}`;
85+
86+
// Highlighted at build time so the page ships no client-side highlighter.
87+
// Shiki reads the code surface from the theme's global token entry, not from
88+
// `colors`, so that is the one repainted in ink to match the panel.
89+
const INK = '#1B1916';
90+
const inkCodeTheme = {
91+
...codeTheme,
92+
name: 'rivet-ink',
93+
colors: { ...codeTheme.colors, 'editor.background': INK },
94+
tokenColors: codeTheme.tokenColors.map((token: any, index: number) =>
95+
index === 0
96+
? { ...token, settings: { ...token.settings, background: INK } }
97+
: token,
98+
),
99+
};
100+
101+
const highlighter = await shiki.getSingletonHighlighter({
102+
langs: ['typescript', 'json'],
103+
themes: [inkCodeTheme],
104+
});
105+
const codeModeHtml = highlighter.codeToHtml(CODE_MODE_EXAMPLE, {
106+
lang: 'typescript',
107+
theme: inkCodeTheme.name,
108+
});
109+
const remoteConfigHtml = highlighter.codeToHtml(REMOTE_CONFIG, {
110+
lang: 'json',
111+
theme: inkCodeTheme.name,
112+
});
113+
const localConfigHtml = highlighter.codeToHtml(LOCAL_CONFIG, {
114+
lang: 'json',
115+
theme: inkCodeTheme.name,
116+
});
117+
118+
const CODE_PLATE_CLASS =
119+
'overflow-x-auto px-5 py-5 font-mono text-[13px] leading-relaxed [&_pre]:!m-0 [&_pre]:!p-0';
120+
121+
const tools = [
122+
{
123+
name: 'search',
124+
title: 'It looks up what it can do',
125+
body: 'Your agent asks what is available in the namespace it is connected to, and gets back the handful of operations that actually apply.',
126+
},
127+
{
128+
name: 'execute',
129+
title: 'Then it writes the code',
130+
body: 'Ordinary JavaScript, so listing your actors, picking the right one, and calling it happen in a single step instead of six round trips.',
131+
},
132+
];
133+
134+
const limits = [
135+
{
136+
title: 'It cannot reach anything else',
137+
body: 'Code runs with no network, no environment variables, no files, and no way to start a process.',
138+
},
139+
{
140+
title: 'Every call goes through Rivet',
141+
body: 'Requests only leave through Rivet, which checks them against what you approved and attaches your credentials for you.',
142+
},
143+
{
144+
title: 'Read-only until you say otherwise',
145+
body: 'A hosted connection starts read-only and its access expires in 15 minutes. Deleting things and touching credentials is never on the table.',
146+
},
147+
];
148+
---
149+
150+
<MarketingLayout
151+
title="MCP - Rivet"
152+
description="Connect AI clients to Rivet Cloud, Rivet Actors, and the Actor Inspector over the Model Context Protocol."
153+
>
154+
<main class="paper-grain font-sans text-ink-soft">
155+
<section class={PRODUCT_HERO_SECTION_CLASS}>
156+
<div class="mx-auto flex w-full max-w-5xl flex-col items-center text-center">
157+
<h1 class={PRODUCT_HERO_H1_CLASS}>Give your agent a real handle on your backend</h1>
158+
159+
<p class="mb-7 max-w-3xl text-base leading-relaxed text-ink-soft md:text-lg">
160+
Connect your AI client to Rivet and it can see what is running, read state,
161+
and call your actors. It writes ordinary JavaScript to do it, in a sandbox
162+
that cannot reach anything you did not hand it.
163+
</p>
164+
165+
<div class={PRODUCT_HERO_CTA_ROW_CLASS}>
166+
<a href="#connect" class={PRODUCT_HERO_PRIMARY_BUTTON_CLASS}>Connect a client</a>
167+
<a href="/talk-to-an-engineer" class={PRODUCT_HERO_SECONDARY_BUTTON_CLASS}>
168+
Talk to an engineer
169+
</a>
170+
</div>
171+
</div>
172+
</section>
173+
174+
<section class="bg-paper px-6 py-16 md:py-24">
175+
<div class="mx-auto max-w-5xl">
176+
<h2 class={SECTION_H2_CLASS}>Two tools instead of forty</h2>
177+
<p class={`${SUBTITLE_CLASS} max-w-2xl`}>
178+
Most MCP servers hand your agent a wall of tools and hope it picks the right
179+
one. Rivet gives it two: one to find out what is there, one to go do it.
180+
</p>
181+
182+
<dl class="mt-10 grid gap-4 md:grid-cols-2">
183+
{tools.map((tool) => (
184+
<div class="rounded-xl border border-ink/10 bg-white/55 p-6">
185+
<dt class="text-base font-medium text-ink">
186+
{tool.title}
187+
<span class="ml-2 font-mono text-[13px] font-normal text-pine">{tool.name}</span>
188+
</dt>
189+
<dd class="mt-2 text-sm leading-relaxed text-ink-soft">{tool.body}</dd>
190+
</div>
191+
))}
192+
</dl>
193+
194+
<InkPanel className="mt-4">
195+
<div class={CODE_PLATE_CLASS} set:html={codeModeHtml} />
196+
</InkPanel>
197+
</div>
198+
</section>
199+
200+
<section id="connect" class="scroll-mt-24 bg-paper px-6 py-16 md:py-24">
201+
<div class="mx-auto max-w-5xl">
202+
<h2 class={SECTION_H2_CLASS}>Add it to your client</h2>
203+
<p class={`${SUBTITLE_CLASS} max-w-2xl`}>
204+
Run one command, then approve the Rivet sign-in your client opens. Anything
205+
else that speaks MCP can point at the endpoint directly.
206+
</p>
207+
208+
<div class="mt-10" data-tabs>
209+
<div class="flex flex-wrap gap-x-6 border-b border-ink/10" role="tablist">
210+
{clients.map((client, index) => (
211+
<button
212+
type="button"
213+
role="tab"
214+
data-tab={index}
215+
aria-selected={index === 0}
216+
class="-mb-px flex items-center gap-2 border-b-2 border-transparent pb-3 text-sm text-ink-faint transition-colors hover:text-ink aria-selected:border-ink aria-selected:font-medium aria-selected:text-ink"
217+
>
218+
<Icon icon={client.icon} className="h-3.5 w-3.5" />
219+
{client.name}
220+
</button>
221+
))}
222+
</div>
223+
224+
{clients.map((client, index) => (
225+
<div data-panel={index} class={index === 0 ? 'pt-6' : 'hidden pt-6'}>
226+
{client.note ? (
227+
<p class="mb-4 max-w-2xl text-sm leading-relaxed text-ink-soft">
228+
{client.note}
229+
</p>
230+
) : null}
231+
{client.command ? (
232+
<InkChip command={client.command} variant="paper" />
233+
) : (
234+
<InkPanel>
235+
<div
236+
class={CODE_PLATE_CLASS}
237+
set:html={client.config === 'local' ? localConfigHtml : remoteConfigHtml}
238+
/>
239+
</InkPanel>
240+
)}
241+
</div>
242+
))}
243+
</div>
244+
245+
<div class="mt-12">
246+
<h3 class="text-base font-medium text-ink">Scope the connection</h3>
247+
<p class="mt-2 max-w-2xl text-sm leading-relaxed text-ink-soft">
248+
Add the target to the endpoint to hold the whole session inside it. Leave
249+
it off and the client can reach everything you can.
250+
</p>
251+
252+
<dl class="mt-5 divide-y divide-ink/10 border-y border-ink/10">
253+
{scopes.map((scope) => (
254+
<div class="flex flex-col gap-1 py-3 sm:flex-row sm:items-baseline sm:justify-between sm:gap-6">
255+
<dt class="overflow-x-auto font-mono text-[13px] text-ink-soft">
256+
<span class="whitespace-nowrap">{scope.url}</span>
257+
</dt>
258+
<dd class="shrink-0 text-sm text-ink">{scope.label}</dd>
259+
</div>
260+
))}
261+
</dl>
262+
</div>
263+
</div>
264+
</section>
265+
266+
<section class="bg-paper px-6 py-16 md:py-24">
267+
<div class="mx-auto max-w-5xl">
268+
<h2 class={SECTION_H2_CLASS}>Model-written code, held at arm's length</h2>
269+
270+
<dl class="mt-10 grid gap-4 md:grid-cols-3">
271+
{limits.map((limit) => (
272+
<div class="rounded-xl border border-ink/10 bg-white/55 p-6">
273+
<dt class="text-base font-medium text-ink">{limit.title}</dt>
274+
<dd class="mt-2 text-sm leading-relaxed text-ink-soft">{limit.body}</dd>
275+
</div>
276+
))}
277+
</dl>
278+
</div>
279+
</section>
280+
281+
<section class="bg-paper px-6 py-16 md:py-24">
282+
<div class="mx-auto grid max-w-5xl gap-8 md:grid-cols-2 md:items-center">
283+
<div>
284+
<h2 class={SECTION_H2_CLASS}>The Inspector, inline</h2>
285+
<p class={SUBTITLE_CLASS}>
286+
Ask your agent about an actor and the Rivet Inspector opens inside the
287+
conversation. Its state, who is connected, what is queued: the same view
288+
you would open in the dashboard, without leaving the chat.
289+
</p>
290+
<p class="mt-4 text-sm leading-relaxed text-ink-soft">
291+
You see the one actor you asked about and nothing else, and it goes away
292+
when the conversation does. Clients that cannot draw it get the same
293+
details written out.
294+
</p>
295+
</div>
296+
297+
<InkPanel>
298+
<div class="space-y-3 px-5 py-6 font-mono text-[13px] text-cream/85">
299+
<p class="text-sage">chat-room · acme</p>
300+
<div class="space-y-2">
301+
{['State', 'Connections', 'Queue', 'Workflow', 'Database'].map((tab) => (
302+
<div class="flex items-center justify-between border-b border-cream/10 pb-2">
303+
<span>{tab}</span>
304+
<span class="text-cream/40">available</span>
305+
</div>
306+
))}
307+
</div>
308+
</div>
309+
</InkPanel>
310+
</div>
311+
</section>
312+
313+
<RedesignedCTA client:visible />
314+
</main>
315+
</MarketingLayout>
316+
317+
<script>
318+
for (const root of document.querySelectorAll('[data-tabs]')) {
319+
const tabs = root.querySelectorAll('[data-tab]');
320+
const panels = root.querySelectorAll('[data-panel]');
321+
for (const tab of tabs) {
322+
tab.addEventListener('click', () => {
323+
for (const other of tabs)
324+
other.setAttribute('aria-selected', String(other === tab));
325+
for (const panel of panels)
326+
panel.classList.toggle('hidden', panel.dataset.panel !== tab.dataset.tab);
327+
});
328+
}
329+
}
330+
</script>

0 commit comments

Comments
 (0)