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: 2 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"three": "^0.183.1",
"vite-plugin-svgr": "^4.5.0"
"vite-plugin-svgr": "^4.5.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@inlang/paraglide-js": "1.11.8",
Expand Down
15 changes: 14 additions & 1 deletion docs/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
Sitemap: https://react-server.dev/sitemap.xml
# @lazarv/react-server is an open-source React Server Components runtime.
# We welcome AI agents — this site is documentation, and we want LLMs to
# index it, cite it, and answer user questions about it accurately.
#
# Content-Signal (https://blog.cloudflare.com/content-signals/):
# search — build a search index and link in search results
# ai-input — use content as live input for AI answers (RAG, in-context)
# ai-train — train or fine-tune AI models

User-agent: *
Content-Signal: search=yes, ai-input=yes, ai-train=yes
Allow: /

Sitemap: https://react-server.dev/sitemap.xml
7 changes: 1 addition & 6 deletions docs/react-server.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ import remarkMath from "remark-math";
export default {
root: "src/pages",
public: "public",
adapter: [
"cloudflare",
{
serverlessFunctions: false,
},
],
adapter: "cloudflare",
mdx: {
remarkPlugins: [remarkGfm, remarkMath],
rehypePlugins: [
Expand Down
124 changes: 124 additions & 0 deletions docs/src/components/WebMCP.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"use client";

import { useEffect } from "react";

/**
* Register WebMCP tools so any browser-based agent (Claude, Cursor, ChatGPT
* Atlas, Cloudflare Browser-Use) interacting with the docs page through
* `navigator.modelContext` can search the docs and fetch any page as
* markdown without scraping HTML.
*
* https://webmcp.org
*/
export default function WebMCP() {
useEffect(() => {
const nav = typeof navigator !== "undefined" ? navigator : null;
if (!nav?.modelContext?.registerTool) return;

const registrations = [];

registrations.push(
nav.modelContext.registerTool({
name: "search_docs",
description:
"Search the @lazarv/react-server documentation for a query and return matching page paths with titles. Use this when the user asks how to do something with @lazarv/react-server.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description:
"Free-text search query (e.g. 'file system router', 'use cache', 'cloudflare deploy').",
},
},
required: ["query"],
},
annotations: { readOnlyHint: true, idempotentHint: true },
async execute({ query }) {
if (typeof query !== "string" || !query.trim()) {
return { error: "Missing query" };
}
// Use the sitemap as a lightweight, cache-friendly index.
const res = await fetch("/sitemap.xml", {
headers: { Accept: "application/xml" },
});
if (!res.ok) return { error: `sitemap fetch failed: ${res.status}` };
const xml = await res.text();
const locs = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map(
(m) => m[1]
);
const q = query.toLowerCase();
const matches = locs
.filter((u) => u.toLowerCase().includes(q))
.slice(0, 20)
.map((u) => ({
url: u,
markdown_url: `${u.replace(/\/$/, "")}.md`,
}));
return { matches, total: matches.length };
},
})
);

registrations.push(
nav.modelContext.registerTool({
name: "get_docs_page",
description:
"Fetch a documentation page as markdown. Pass either a full URL on https://react-server.dev or a path like '/router/file-router'. Returns the page content as text/markdown.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description:
"Page path (e.g. '/router/file-router') or full URL. The .md suffix is added automatically.",
},
},
required: ["path"],
},
annotations: { readOnlyHint: true, idempotentHint: true },
async execute({ path }) {
if (typeof path !== "string" || !path) {
return { error: "Missing path" };
}
let target = path;
try {
const u = new URL(path, location.origin);
target = u.pathname;
} catch {
// not a URL, treat as path
}
if (!target.startsWith("/")) target = `/${target}`;
target = target.replace(/\/$/, "");
if (!target.endsWith(".md")) target = `${target}.md`;
const res = await fetch(target, {
headers: { Accept: "text/markdown" },
});
if (!res.ok) return { error: `fetch failed: ${res.status}` };
const markdown = await res.text();
return { path: target, markdown };
},
})
);

return () => {
for (const r of registrations) {
if (typeof r === "function") {
try {
r();
} catch {
/* ignore */
}
} else if (r && typeof r.unregister === "function") {
try {
r.unregister();
} catch {
/* ignore */
}
}
}
};
}, []);

return null;
}
161 changes: 161 additions & 0 deletions docs/src/pages/(agent-discovery).middleware.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { setHeader } from "@lazarv/react-server";
import { useUrl } from "@lazarv/react-server";

import skillContent from "../../../skills/react-server/SKILL.md?raw";
import { version } from "../version.mjs";

// ---------------------------------------------------------------------------
// Agent-readiness payloads
//
// Implements the contracts checked by https://isitagentready.com:
// - RFC 9727 API Catalog → /.well-known/api-catalog
// - Agent Skills v0.2 index → /.well-known/agent-skills/index.json
// - Agent Skill body → /.well-known/agent-skills/react-server/SKILL.md
// - MCP Server Card → /.well-known/mcp/server-card.json
// - RFC 8288 Link headers → on every documentation page
// ---------------------------------------------------------------------------

const SITE = "https://react-server.dev";

const apiCatalog = {
linkset: [
{
anchor: `${SITE}/.well-known/api-catalog`,
item: [
{ href: `${SITE}/mcp` },
{ href: `${SITE}/llms.txt` },
{ href: `${SITE}/sitemap.xml` },
{ href: `${SITE}/schema.json` },
],
},
{
anchor: `${SITE}/mcp`,
"service-doc": [
{ href: `${SITE}/features/mcp`, type: "text/html" },
{ href: `${SITE}/features/mcp.md`, type: "text/markdown" },
],
describedby: [
{
href: `${SITE}/.well-known/mcp/server-card.json`,
type: "application/json",
},
],
},
{
anchor: `${SITE}/llms.txt`,
describedby: [{ href: `${SITE}/llms.txt`, type: "text/plain" }],
},
{
anchor: `${SITE}/schema.json`,
describedby: [
{ href: `${SITE}/schema.json`, type: "application/schema+json" },
],
},
],
};

const agentSkillsIndex = {
$schema: "https://agent-skills.dev/schema/v0.2.0.json",
skills: [
{
name: "react-server",
description:
"Build applications with @lazarv/react-server — a React Server Components runtime built on Vite. Covers use directives, file-system router, HTTP hooks, caching, live components, workers, MCP, deployment, and all core APIs.",
version,
skill_url: `${SITE}/.well-known/agent-skills/react-server/SKILL.md`,
homepage: SITE,
license: "MIT",
},
],
};

const mcpServerCard = {
$schema:
"https://modelcontextprotocol.io/schemas/draft/2025-09-29/server-card.json",
name: "react-server-docs",
title: "@lazarv/react-server Documentation",
description:
"Search and read the @lazarv/react-server documentation as Model Context Protocol resources and tools. Provides a search_docs tool and exposes every documentation page as a markdown resource.",
version,
homepage: SITE,
documentation: `${SITE}/features/mcp`,
endpoints: {
streamable_http: `${SITE}/mcp`,
},
capabilities: {
tools: { listChanged: false },
resources: { listChanged: false, subscribe: false },
prompts: { listChanged: false },
},
contact: {
repository: "https://github.com/lazarv/react-server",
},
};

const wellKnown = {
"/.well-known/api-catalog": () =>
json(
apiCatalog,
'application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"'
),
"/.well-known/agent-skills/index.json": () => json(agentSkillsIndex),
"/.well-known/agent-skills/react-server/SKILL.md": () =>
new Response(skillContent, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Cache-Control": "public, max-age=3600",
},
}),
"/.well-known/mcp/server-card.json": () => json(mcpServerCard),
};

function json(body, contentType = "application/json; charset=utf-8") {
return new Response(JSON.stringify(body, null, 2), {
headers: {
"Content-Type": contentType,
"Cache-Control": "public, max-age=3600",
},
});
}

// ---------------------------------------------------------------------------
// Discovery Link headers (RFC 8288 / RFC 9727)
//
// Advertised on every documentation page so any HTTP client (including agents
// that only do HEAD or GET on `/`) can discover the API catalog, MCP entry,
// and human-/machine-readable documentation without crawling the whole site.
// ---------------------------------------------------------------------------

const linkHeader = [
'</.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json"',
'</mcp>; rel="service-meta"; type="application/json"',
'</llms.txt>; rel="describedby"; type="text/plain"',
'</sitemap.xml>; rel="sitemap"; type="application/xml"',
'</.well-known/agent-skills/index.json>; rel="https://agent-skills.dev/rel/index"; type="application/json"',
].join(", ");

// Pathnames that should never receive the discovery Link header — they're
// machine-only endpoints with their own headers/cache semantics.
const SKIP_LINK_HEADER = (pathname) =>
pathname === "/sitemap.xml" ||
pathname === "/schema.json" ||
pathname === "/mcp" ||
pathname.startsWith("/mcp/") ||
pathname.startsWith("/.well-known/") ||
pathname.startsWith("/md/") ||
pathname.endsWith(".md");

export default function AgentDiscovery() {
const { pathname } = useUrl();

// 1. Static well-known endpoints — short-circuit with the right content type.
const wellKnownHandler = wellKnown[pathname];
if (wellKnownHandler) {
return wellKnownHandler();
}

// 2. Set discovery Link header on documentation pages.
if (!SKIP_LINK_HEADER(pathname)) {
setHeader("Link", linkHeader);
}
}
Loading
Loading