From 33eb238c8507647f2f775c42b8eeba231e432a3a Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Tue, 1 Sep 2026 13:12:50 -0700 Subject: [PATCH] feat(website): move MCP guide into docs --- .gitignore | 1 + Caddyfile | 37 +++- examples/docs/mcp/call-actor-action.txt | 1 + examples/docs/mcp/claude-code.sh | 1 + examples/docs/mcp/codex.sh | 1 + examples/docs/mcp/cursor.json | 7 + examples/docs/mcp/find-running-actors.txt | 1 + examples/docs/mcp/gemini-cli.sh | 1 + examples/docs/mcp/inspect-actor-inline.txt | 1 + examples/docs/mcp/local.json | 8 + examples/docs/mcp/one-request.js | 10 + examples/docs/mcp/vs-code.sh | 1 + src/components/Footer.jsx | 1 + src/components/Tabs.tsx | 33 +++- src/components/TabsScript.astro | 212 +++++++++++++++------ src/components/docs/DocsArticlePage.astro | 43 ++++- src/components/v2/Code.tsx | 16 +- src/components/v2/Header.tsx | 30 ++- src/content/docs/docs/mcp.mdx | 127 ++++++++++++ src/layouts/BaseLayout.astro | 4 +- src/layouts/DocsLayout.astro | 5 +- src/lib/routeSeoPolicy.ts | 4 +- src/metadata/mdx-to-markdown.ts | 2 +- src/pages/[product]/[tab]/[...slug].astro | 52 ++++- src/pages/docs/[...slug].astro | 43 +++++ src/pages/docs/index.astro | 12 +- src/pages/metadata/docs.json.ts | 25 ++- src/sitemap/ai.ts | 79 ++++++++ src/sitemap/docs-sources.node.ts | 14 +- src/sitemap/docs-sources.ts | 19 +- src/sitemap/mod.ts | 2 - 31 files changed, 688 insertions(+), 105 deletions(-) create mode 100644 examples/docs/mcp/call-actor-action.txt create mode 100644 examples/docs/mcp/claude-code.sh create mode 100644 examples/docs/mcp/codex.sh create mode 100644 examples/docs/mcp/cursor.json create mode 100644 examples/docs/mcp/find-running-actors.txt create mode 100644 examples/docs/mcp/gemini-cli.sh create mode 100644 examples/docs/mcp/inspect-actor-inline.txt create mode 100644 examples/docs/mcp/local.json create mode 100644 examples/docs/mcp/one-request.js create mode 100644 examples/docs/mcp/vs-code.sh create mode 100644 src/content/docs/docs/mcp.mdx create mode 100644 src/pages/docs/[...slug].astro create mode 100644 src/sitemap/ai.ts diff --git a/.gitignore b/.gitignore index fcf7b09e..bc84f497 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ src/content/docs/workflows /public/agentos/ /public/cloud/ /public/dynamic-apps/ +/public/docs/ /public/workflows/ /public/metadata/ diff --git a/Caddyfile b/Caddyfile index cd2133d9..53d38dae 100644 --- a/Caddyfile +++ b/Caddyfile @@ -59,9 +59,44 @@ # www -> apex. Railway terminates TLS upstream, so always redirect to https. # Only takes effect when www.rivet.dev is attached to the Railway service. - @www host www.rivet.dev + @www { + host www.rivet.dev + not path /mcp /mcp/ + } redir @www https://rivet.dev{uri} 301 + # The former apex landing route now lives in the docs. Keep this redirect + # host-aware: an unconditional /mcp redirect would intercept the MCP service + # endpoint on mcp.rivet.dev before its reverse-proxy handler runs. + @mcpDocsPage { + not host mcp.rivet.dev + path /mcp /mcp/ + } + redir @mcpDocsPage https://rivet.dev/docs/mcp/{http.request.uri.prefixed_query} 301 + + # mcp.rivet.dev redirects its root to the canonical docs page while the MCP + # server keeps every route it owns, which are proxied to MCP_UPSTREAM. A + # blanket host redirect is not possible: https://mcp.rivet.dev/mcp is the + # endpoint clients connect to. Only takes effect when mcp.rivet.dev is + # attached to the Railway service and MCP_UPSTREAM is set. + @mcpHost host mcp.rivet.dev + handle @mcpHost { + @mcpService path /mcp /mcp/* /.well-known/oauth-protected-resource /health /ready + handle @mcpService { + reverse_proxy {$MCP_UPSTREAM} + } + + handle / { + redir https://rivet.dev/docs/mcp/{http.request.uri.prefixed_query} 301 + } + + # The service routes above are the only content this host serves; the rest + # of the site keeps one canonical home. + handle { + redir https://rivet.dev{uri} 301 + } + } + # Real HTTP 301s for legacy/moved URLs. Generated from redirects.mjs by # scripts/generate-caddy-redirects.mjs. The import path resolves relative to # this Caddyfile, so it works both locally and in-container. diff --git a/examples/docs/mcp/call-actor-action.txt b/examples/docs/mcp/call-actor-action.txt new file mode 100644 index 00000000..d089823b --- /dev/null +++ b/examples/docs/mcp/call-actor-action.txt @@ -0,0 +1 @@ +What has Acme been saying? diff --git a/examples/docs/mcp/claude-code.sh b/examples/docs/mcp/claude-code.sh new file mode 100644 index 00000000..28ad4e58 --- /dev/null +++ b/examples/docs/mcp/claude-code.sh @@ -0,0 +1 @@ +claude mcp add --transport http rivet https://mcp.rivet.dev/mcp diff --git a/examples/docs/mcp/codex.sh b/examples/docs/mcp/codex.sh new file mode 100644 index 00000000..ce3f462b --- /dev/null +++ b/examples/docs/mcp/codex.sh @@ -0,0 +1 @@ +codex mcp add rivet --url https://mcp.rivet.dev/mcp diff --git a/examples/docs/mcp/cursor.json b/examples/docs/mcp/cursor.json new file mode 100644 index 00000000..132bd185 --- /dev/null +++ b/examples/docs/mcp/cursor.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "rivet": { + "url": "https://mcp.rivet.dev/mcp" + } + } +} diff --git a/examples/docs/mcp/find-running-actors.txt b/examples/docs/mcp/find-running-actors.txt new file mode 100644 index 00000000..4180f562 --- /dev/null +++ b/examples/docs/mcp/find-running-actors.txt @@ -0,0 +1 @@ +Which chat rooms are still awake? diff --git a/examples/docs/mcp/gemini-cli.sh b/examples/docs/mcp/gemini-cli.sh new file mode 100644 index 00000000..7167165a --- /dev/null +++ b/examples/docs/mcp/gemini-cli.sh @@ -0,0 +1 @@ +gemini mcp add --transport http rivet https://mcp.rivet.dev/mcp diff --git a/examples/docs/mcp/inspect-actor-inline.txt b/examples/docs/mcp/inspect-actor-inline.txt new file mode 100644 index 00000000..33409643 --- /dev/null +++ b/examples/docs/mcp/inspect-actor-inline.txt @@ -0,0 +1 @@ +Open the Actor Inspector for chat-room · acme. diff --git a/examples/docs/mcp/local.json b/examples/docs/mcp/local.json new file mode 100644 index 00000000..1dc06c84 --- /dev/null +++ b/examples/docs/mcp/local.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "rivet": { + "command": "npx", + "args": ["-y", "@rivet-dev/mcp", "--target", "local"] + } + } +} diff --git a/examples/docs/mcp/one-request.js b/examples/docs/mcp/one-request.js new file mode 100644 index 00000000..6b916587 --- /dev/null +++ b/examples/docs/mcp/one-request.js @@ -0,0 +1,10 @@ +// "Which chat rooms are still awake, and what has Acme been saying?" +const { actors } = await rivet.actors.list({ name: "chat-room" }); +const awake = actors.filter((room) => room.status === "running"); + +const history = await rivet.actor.action({ + actor: { name: "chat-room", key: ["acme"] }, + name: "getHistory", +}); + +return { awake: awake.length, history }; diff --git a/examples/docs/mcp/vs-code.sh b/examples/docs/mcp/vs-code.sh new file mode 100644 index 00000000..40793952 --- /dev/null +++ b/examples/docs/mcp/vs-code.sh @@ -0,0 +1 @@ +code --add-mcp '{"name":"rivet","type":"http","url":"https://mcp.rivet.dev/mcp"}' diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 6ad153c1..e6f4cf37 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -33,6 +33,7 @@ const footer = { ], devs: [ { name: "Documentation", href: "/docs" }, + { name: "MCP", href: "https://rivet.dev/docs/mcp/" }, { name: "Changelog", href: "/blog/" }, { name: "Status Page", href: "https://rivet.betteruptime.com/" }, ], diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index a1cd6667..7b3d3c13 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -1,19 +1,30 @@ import { cn } from "@rivet-gg/components"; +import { Icon } from "@rivet-gg/icons"; +import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import type { ReactNode } from "react"; interface TabProps { title: string; + icon?: IconDefinition; children: ReactNode; } -export const Tab = ({ title, children }: TabProps) => { +export const Tab = ({ title, icon, children }: TabProps) => { return ( -
+
+

+ {icon && } + {title} +

-
+
{children}
@@ -32,21 +44,24 @@ export const Tab = ({ title, children }: TabProps) => { interface TabsProps { children: ReactNode; + label?: string; } -export const Tabs = ({ children }: TabsProps) => { +export const Tabs = ({ children, label = "Options" }: TabsProps) => { return (
-
+ -
+ -
+
{children}
diff --git a/src/components/TabsScript.astro b/src/components/TabsScript.astro index 045ddf7d..0d6dca6e 100644 --- a/src/components/TabsScript.astro +++ b/src/components/TabsScript.astro @@ -4,45 +4,129 @@ --- diff --git a/src/components/docs/DocsArticlePage.astro b/src/components/docs/DocsArticlePage.astro index fe893176..894ed169 100644 --- a/src/components/docs/DocsArticlePage.astro +++ b/src/components/docs/DocsArticlePage.astro @@ -12,7 +12,8 @@ import { DocsLanding } from '@/components/docs/DocsLanding'; import { docsLandings } from '@/components/docs/docsLandings'; import { Icon, faPencil } from '@rivet-gg/icons'; import { sitemap } from '@/sitemap/mod'; -import { findActiveTab } from '@/lib/sitemap'; +import { withAiSidebarSection } from '@/sitemap/ai'; +import { findActiveTab, findPageForHref } from '@/lib/sitemap'; import { getContentSlugPath } from '@/lib/content-path'; import { productAccent } from '@/lib/product-accent'; import { getProductMetadata } from '@/sitemap/product-metadata'; @@ -38,6 +39,10 @@ const { // copy points search engines at one winner instead of letting Google pick. canonicalUrl: canonicalUrlOverride, robots, + exactTitle = false, + routeSlugOverride, + markdownPathOverride, + editUrlOverride, // Extra MDX components for this page, e.g. the bound product slot renderer. extraComponents = {}, } = Astro.props; @@ -85,22 +90,39 @@ const routeEntryId = entryIdPrefix && entry.id.startsWith(entryIdPrefix) : entryIdPrefix && `${entry.id}/` === entryIdPrefix ? '' : entry.id; -const slugPath = getContentSlugPath(routeEntryId); +const slugPath = routeSlugOverride ?? getContentSlugPath(routeEntryId); // Section overviews render as a custom icon-grid landing instead of the // standard prose article. The markdown chrome (edit button, breadcrumb, TOC, // page dropdown) is suppressed for these. const landing = docsLandings[entrySlugPath ?? '']; const isLanding = Boolean(landing); const fullPath = slugPath ? `${routePrefix}/${slugPath}/` : `${routePrefix}/`; -const foundTab = findActiveTab(fullPath, sitemap); -const rawParentPage = foundTab?.page?.parent; +const discovered = findActiveTab(fullPath, sitemap); +const routeOwnedTab = productId && tabId + ? sitemap.find((tab) => tab.href.replace(/\/$/, '') === `/${productId}/${tabId}`) + : undefined; +const activeTab = routeOwnedTab ?? discovered?.tab; +const sourceSidebar = activeTab?.sidebar ?? []; +const sidebar = tabId === 'docs' || fullPath.startsWith('/docs/') + ? withAiSidebarSection(sourceSidebar, productId) + : sourceSidebar; +const activePage = activeTab + ? findPageForHref(fullPath, { ...activeTab, sidebar }) + : discovered?.page; +const discoveredParentPage = activePage?.parent; +const rawParentPage = + discoveredParentPage && + 'title' in discoveredParentPage && + discoveredParentPage.title === 'AI Integration' + ? { ...discoveredParentPage, title: 'AI Tools' } + : discoveredParentPage; // A section's overview page sits inside a sidebar section of the same name, so // the eyebrow would just repeat the h1 ("Control Plane" over "Control Plane"). const parentPage = rawParentPage && 'title' in rawParentPage && rawParentPage.title === title ? undefined : rawParentPage; -const hasSidebar = (foundTab?.tab?.sidebar?.length ?? 0) > 0; +const hasSidebar = sidebar.length > 0; const canonicalUrl = canonicalUrlOverride ?? `https://rivet.dev${fullPath}`; // Inside a product vertical, prose links carry that product's accent instead of @@ -111,7 +133,7 @@ const proseStyle = accent ? { '--tw-prose-links': accent.hex } : undefined; // The Markdown mirror written by `pnpm gen:markdown` sits at the page's own // site path plus `.md`, so the dropdown's link is just this route unslashed. -const markdownPath = fullPath.replace(/^\/|\/$/g, ''); +const markdownPath = markdownPathOverride ?? fullPath.replace(/^\/|\/$/g, ''); const componentSourcePath = entry.filePath ? entry.filePath.replace(/^.*?src\/content\//, '') : `docs/${entry.id}.mdx`; @@ -128,11 +150,11 @@ const productContentPath = normalizedFilePath?.includes('/docs/content/') : productContentMarker && normalizedFilePath?.includes(productContentMarker) ? normalizedFilePath.slice(normalizedFilePath.indexOf(productContentMarker) + productContentMarker.length) : `${tabId}/${routeEntryId ? `${routeEntryId}.mdx` : 'index.mdx'}`; -const editUrl = product && tabId !== 'self-host' +const editUrl = editUrlOverride ?? (product && tabId !== 'self-host' ? product.localBundle ? `https://github.com/rivet-dev/website/edit/main/${product.localBundle}/docs/content/${productContentPath}` : `https://github.com/rivet-dev/${product.repo}/edit/main/docs/content/${productContentPath}` - : `https://github.com/rivet-dev/website/edit/main/src/content/${componentSourcePath}`; + : `https://github.com/rivet-dev/website/edit/main/src/content/${componentSourcePath}`); // Build BreadcrumbList schema const breadcrumbItems = [ @@ -163,15 +185,16 @@ const breadcrumbSchema = { description={seo.description} canonicalUrl={canonicalUrl} robots={robots} - sidebar={foundTab?.tab?.sidebar} + sidebar={sidebar} productId={productId} tabId={tabId} sectionLabel={sectionLabel} + exactTitle={exactTitle} >