diff --git a/web/app/auth/RelayauthContent.tsx b/web/app/auth/RelayauthContent.tsx
index 017f6c7..0ffec38 100644
--- a/web/app/auth/RelayauthContent.tsx
+++ b/web/app/auth/RelayauthContent.tsx
@@ -69,7 +69,7 @@ const whyCards = [
const getStartedSteps = [
{
title: 'Create an identity',
- code: `curl -X POST https://api.relayauth.dev/v1/identities \\
+ code: `curl -X POST "$RELAYAUTH_BASE_URL/v1/identities" \\
-H "content-type: application/json" \\
-d '{
"name": "billing-bot",
@@ -80,7 +80,7 @@ const getStartedSteps = [
},
{
title: 'Issue a token',
- code: `curl -X POST https://api.relayauth.dev/v1/tokens \\
+ code: `curl -X POST "$RELAYAUTH_BASE_URL/v1/tokens" \\
-H "content-type: application/json" \\
-d '{
"identity_id": "agent_8x2k",
@@ -90,7 +90,7 @@ const getStartedSteps = [
},
{
title: 'Verify at the edge',
- code: `curl https://api.relayauth.dev/.well-known/jwks.json
+ code: `curl "$RELAYAUTH_BASE_URL/.well-known/jwks.json"
# Then validate locally and enforce:
# stripe:orders:read`,
@@ -150,7 +150,7 @@ auth.authorize(
token=token["access_token"],
scope="stripe:orders:read",
)`,
- curl: `curl -X POST https://api.relayauth.dev/v1/identities \\
+ curl: `curl -X POST "$RELAYAUTH_BASE_URL/v1/identities" \\
-H "authorization: Bearer $RELAYAUTH_API_KEY" \\
-H "content-type: application/json" \\
-d '{
@@ -160,7 +160,7 @@ auth.authorize(
"sponsor_id": "user_jane"
}'
-curl -X POST https://api.relayauth.dev/v1/tokens \\
+curl -X POST "$RELAYAUTH_BASE_URL/v1/tokens" \\
-H "authorization: Bearer $RELAYAUTH_API_KEY" \\
-H "content-type: application/json" \\
-d '{
@@ -169,7 +169,7 @@ curl -X POST https://api.relayauth.dev/v1/tokens \\
"ttl": "1h"
}'
-curl https://api.relayauth.dev/.well-known/jwks.json`,
+curl "$RELAYAUTH_BASE_URL/.well-known/jwks.json"`,
};
function highlight(code: string, tab: SdkTab) {
diff --git a/web/app/file/RelayfileContent.tsx b/web/app/file/RelayfileContent.tsx
index 724c3a4..3fc520f 100644
--- a/web/app/file/RelayfileContent.tsx
+++ b/web/app/file/RelayfileContent.tsx
@@ -162,13 +162,13 @@ await run(agent, 'Review /digests/yesterday.md and file Linear follow-ups.');`,
curl: {
label: 'cURL',
language: 'curl',
- code: `curl "https://api.agentrelay.com/relayfile/v1/workspaces/rw_123/fs/tree?path=/" \\
+ code: `curl "https://file.agentrelay.com/v1/workspaces/rw_123/fs/tree?path=/" \\
-H "authorization: Bearer $RELAYFILE_TOKEN"
-curl "https://api.agentrelay.com/relayfile/v1/workspaces/rw_123/fs/file?path=/digests/yesterday.md" \\
+curl "https://file.agentrelay.com/v1/workspaces/rw_123/fs/file?path=/digests/yesterday.md" \\
-H "authorization: Bearer $RELAYFILE_TOKEN"
-curl -X PUT "https://api.agentrelay.com/relayfile/v1/workspaces/rw_123/fs/file?path=/linear/issues/AGE-12.json" \\
+curl -X PUT "https://file.agentrelay.com/v1/workspaces/rw_123/fs/file?path=/linear/issues/AGE-12.json" \\
-H "authorization: Bearer $RELAYFILE_TOKEN" \\
-H "content-type: application/json" \\
-d '{"contentType":"application/json","content":"{\\"state\\":\\"In Review\\"}"}'`,
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
index 1d8b277..0dd8e93 100644
--- a/web/app/layout.tsx
+++ b/web/app/layout.tsx
@@ -4,7 +4,7 @@ import { Geist_Mono, Inter, Sora } from 'next/font/google';
import type { ReactNode } from 'react';
import { defaultOgImage } from '../lib/og-meta';
-import { POSTHOG_HOST, SITE_URL } from '../lib/site';
+import { absoluteUrl, POSTHOG_HOST, SITE_EMAIL, SITE_NAME, SITE_URL } from '../lib/site';
import { WebsitePostHogPageView } from './PostHogPageView';
import './globals.css';
@@ -62,6 +62,53 @@ export const metadata: Metadata = {
},
};
+// Site-wide structured data. Individual pages add their own Article/Service/
+// CollectionPage nodes and reference these two by @id, so an agent reading any
+// single page still resolves the publisher and the site root.
+const siteStructuredData = {
+ '@context': 'https://schema.org',
+ '@graph': [
+ {
+ '@type': 'Organization',
+ '@id': `${SITE_URL}/#organization`,
+ name: SITE_NAME,
+ url: SITE_URL,
+ email: SITE_EMAIL,
+ logo: {
+ '@type': 'ImageObject',
+ url: absoluteUrl('/agent-relay-logo-white.svg'),
+ },
+ description:
+ 'Agent Relay is the messaging layer for AI agents: channels, DMs, durable delivery, event listeners, and typed actions for any agent runtime.',
+ sameAs: [
+ 'https://github.com/agentworkforce/relay',
+ 'https://twitter.com/agent_relay',
+ 'https://discord.gg/RJGE7CHV',
+ ],
+ },
+ {
+ '@type': 'WebSite',
+ '@id': `${SITE_URL}/#website`,
+ name: SITE_NAME,
+ url: SITE_URL,
+ inLanguage: 'en-US',
+ publisher: { '@id': `${SITE_URL}/#organization` },
+ },
+ {
+ '@type': 'SoftwareApplication',
+ '@id': `${SITE_URL}/#software`,
+ name: SITE_NAME,
+ url: SITE_URL,
+ applicationCategory: 'DeveloperApplication',
+ operatingSystem: 'Any',
+ publisher: { '@id': `${SITE_URL}/#organization` },
+ description:
+ 'Add channels, DMs, durable delivery, event listeners, and Zod-typed actions to any agent runtime.',
+ softwareHelp: { '@type': 'CreativeWork', url: absoluteUrl('/docs') },
+ },
+ ],
+};
+
export default function RootLayout({ children }: { children: ReactNode }) {
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const content = postHogKey ? (
@@ -83,7 +130,28 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return (
+
+ {/* Machine-readable mirrors of this site, advertised on every page so an
+ agent that lands anywhere can find the plain-text and feed formats. */}
+
+
+
+
+
{content}
diff --git a/web/app/message/RelaycastContent.tsx b/web/app/message/RelaycastContent.tsx
index 643ccf1..1b4913f 100644
--- a/web/app/message/RelaycastContent.tsx
+++ b/web/app/message/RelaycastContent.tsx
@@ -98,12 +98,12 @@ for event in client.events.stream(channel="dev"):
},
curl: {
label: 'cURL',
- code: `TOKEN=$(curl -s -X POST https://api.relaycast.dev/v1/agents \\
+ code: `TOKEN=$(curl -s -X POST https://cast.agentrelay.com/v1/agents \\
-H "Authorization: Bearer rk_live_..." \\
-H "Content-Type: application/json" \\
-d '{"name":"Bot","type":"agent"}' | jq -r .data.token)
-curl -X POST https://api.relaycast.dev/v1/channels/general/messages \\
+curl -X POST https://cast.agentrelay.com/v1/channels/general/messages \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{"text":"Hello from cURL!"}'`,
@@ -134,14 +134,14 @@ const steps = [
{
step: '01',
title: 'Create a workspace',
- code: `curl -X POST https://api.relaycast.dev/v1/workspaces \\
+ code: `curl -X POST https://cast.agentrelay.com/v1/workspaces \\
-H "Content-Type: application/json" \\
-d '{"name": "my-project"}'`,
},
{
step: '02',
title: 'Register your agents',
- code: `curl -X POST https://api.relaycast.dev/v1/agents \\
+ code: `curl -X POST https://cast.agentrelay.com/v1/agents \\
-H "Authorization: Bearer rk_live_YOUR_KEY" \\
-H "Content-Type: application/json" \\
-d '{"name": "Alice", "type": "agent"}'`,
@@ -149,7 +149,7 @@ const steps = [
{
step: '03',
title: 'Start talking',
- code: `curl -X POST https://api.relaycast.dev/v1/channels/general/messages \\
+ code: `curl -X POST https://cast.agentrelay.com/v1/channels/general/messages \\
-H "Authorization: Bearer at_live_TOKEN" \\
-H "Content-Type: application/json" \\
-d '{"text": "Hello from Alice!"}'`,
diff --git a/web/app/primitives/PrimitivesContent.tsx b/web/app/primitives/PrimitivesContent.tsx
index d5ed569..469330f 100644
--- a/web/app/primitives/PrimitivesContent.tsx
+++ b/web/app/primitives/PrimitivesContent.tsx
@@ -148,7 +148,7 @@ const primitives = [
},
],
docsHref: '/docs',
- githubHref: 'https://app.agentcron.dev',
+ githubHref: 'https://github.com/AgentWorkforce/relaycron',
},
];
diff --git a/web/app/robots.ts b/web/app/robots.ts
index 2fde2af..b525930 100644
--- a/web/app/robots.ts
+++ b/web/app/robots.ts
@@ -2,13 +2,44 @@ import type { MetadataRoute } from 'next';
import { absoluteUrl, SITE_URL } from '../lib/site';
+// Named allow rules for the crawlers and agent fetchers that read their own
+// user-agent out of robots.txt. Everything here is already covered by the
+// permissive `*` rule below; the explicit entries exist because several of
+// these bots treat "no rule for me" more conservatively than an explicit
+// Allow, and because a scanner reading robots.txt can only see the agents that
+// are named.
const AI_CRAWLERS = [
+ // OpenAI
+ 'GPTBot',
'OAI-SearchBot',
'ChatGPT-User',
- 'GPTBot',
+ // Anthropic
+ 'ClaudeBot',
'Claude-SearchBot',
'Claude-User',
- 'ClaudeBot',
+ 'anthropic-ai',
+ // Google
+ 'Google-Extended',
+ 'GoogleOther',
+ // Perplexity
+ 'PerplexityBot',
+ 'Perplexity-User',
+ // Apple
+ 'Applebot',
+ 'Applebot-Extended',
+ // Meta
+ 'meta-externalagent',
+ 'Meta-ExternalFetcher',
+ // Microsoft / Bing
+ 'bingbot',
+ // Others
+ 'Amazonbot',
+ 'Bytespider',
+ 'CCBot',
+ 'cohere-ai',
+ 'DuckAssistBot',
+ 'MistralAI-User',
+ 'YouBot',
];
export default function robots(): MetadataRoute.Robots {
@@ -20,17 +51,10 @@ export default function robots(): MetadataRoute.Robots {
})),
{
userAgent: '*',
- allow: [
- '/',
- '/skill',
- '/skill.md',
- '/openclaw',
- '/openclaw/skill',
- '/agents',
- '/agents/',
- '/docs/',
- '/blog/',
- ],
+ allow: ['/'],
+ // Internal target of the /.well-known rewrite; the dot-prefixed paths
+ // are the canonical ones.
+ disallow: ['/well-known/'],
},
],
sitemap: absoluteUrl('/sitemap.xml'),
diff --git a/web/app/schedule/ScheduleContent.tsx b/web/app/schedule/ScheduleContent.tsx
index 4373b7c..b73d9e3 100644
--- a/web/app/schedule/ScheduleContent.tsx
+++ b/web/app/schedule/ScheduleContent.tsx
@@ -67,7 +67,7 @@ cron.schedules.create(
for event in cron.events():
print(f"Job fired: {event.schedule_name} at {event.fired_at}")`,
curl: `# Create a schedule via REST API
-curl -X POST https://api.agentcron.dev/v1/schedules \\
+curl -X POST "$RELAY_CRON_BASE_URL/v1/schedules" \\
-H "Authorization: Bearer $RELAY_CRON_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
@@ -108,7 +108,12 @@ curl -X POST https://api.agentcron.dev/v1/schedules \\
on Cloudflare Durable Objects.
-
+
Start building free
diff --git a/web/app/well-known/api-catalog/route.ts b/web/app/well-known/api-catalog/route.ts
new file mode 100644
index 0000000..e199599
--- /dev/null
+++ b/web/app/well-known/api-catalog/route.ts
@@ -0,0 +1,14 @@
+import { getApiCatalog } from '../../../lib/agent-discovery';
+
+// RFC 9727 API catalog, served at /.well-known/api-catalog via the rewrite in
+// next.config.mjs.
+export const revalidate = 86400;
+
+export function GET() {
+ return new Response(JSON.stringify(getApiCatalog(), null, 2), {
+ headers: {
+ 'Content-Type': 'application/linkset+json',
+ 'Cache-Control': 'public, max-age=86400',
+ },
+ });
+}
diff --git a/web/app/well-known/mcp.json/route.ts b/web/app/well-known/mcp.json/route.ts
new file mode 100644
index 0000000..57bfa7a
--- /dev/null
+++ b/web/app/well-known/mcp.json/route.ts
@@ -0,0 +1,14 @@
+import { getMcpManifest } from '../../../lib/agent-discovery';
+
+// MCP server manifest, served at /.well-known/mcp.json via the rewrite in
+// next.config.mjs.
+export const revalidate = 86400;
+
+export function GET() {
+ return new Response(JSON.stringify(getMcpManifest(), null, 2), {
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ 'Cache-Control': 'public, max-age=86400',
+ },
+ });
+}
diff --git a/web/app/well-known/security.txt/route.ts b/web/app/well-known/security.txt/route.ts
new file mode 100644
index 0000000..5ded7df
--- /dev/null
+++ b/web/app/well-known/security.txt/route.ts
@@ -0,0 +1,18 @@
+import { getSecurityTxt } from '../../../lib/agent-discovery';
+
+// Served at /.well-known/security.txt via the rewrite in next.config.mjs — the
+// App Router skips dot-prefixed directories, so the route lives at
+// /well-known/ and the canonical path rewrites onto it.
+//
+// ISR rather than force-static so the Expires field rolls forward instead of
+// freezing at build time.
+export const revalidate = 86400;
+
+export function GET() {
+ return new Response(getSecurityTxt(new Date()), {
+ headers: {
+ 'Content-Type': 'text/plain; charset=utf-8',
+ 'Cache-Control': 'public, max-age=86400',
+ },
+ });
+}
diff --git a/web/components/home/A2AFeature.tsx b/web/components/home/A2AFeature.tsx
index 2320ef1..e3665b4 100644
--- a/web/components/home/A2AFeature.tsx
+++ b/web/components/home/A2AFeature.tsx
@@ -30,7 +30,7 @@ const AGENT_CARD_TOKENS: ReadonlyArray<{ text: string; kind?: TokenKind }> = [
{ text: ',\n ' },
{ text: '"url"', kind: 'var' },
{ text: ': ' },
- { text: '"https://relay.dev/a2a/scout"', kind: 'str' },
+ { text: '"https://cast.agentrelay.com/a2a/scout"', kind: 'str' },
{ text: ',\n ' },
{ text: '"capabilities"', kind: 'var' },
{ text: ': { ' },
diff --git a/web/components/home/ContextCapabilities.tsx b/web/components/home/ContextCapabilities.tsx
index 46379e1..6178071 100644
--- a/web/components/home/ContextCapabilities.tsx
+++ b/web/components/home/ContextCapabilities.tsx
@@ -4,10 +4,13 @@ import { SearchPreviewAnimation } from '../../app/SearchPreviewAnimation';
import s from '../../app/landing.module.css';
import { WaveDivider } from './icons';
-const WEBHOOK_SNIPPET = `curl -X POST \\
- https://api.agentrelay.com/v1/webhooks \\
+// relay.webhooks.createInbound() mints the URL and token per webhook, so there
+// is no fixed path to show here — the payload shape and bearer auth are what
+// the snippet is illustrating. See content/docs/webhooks.mdx.
+const WEBHOOK_SNIPPET = `curl -X POST "$RELAY_INBOUND_WEBHOOK_URL" \\
+ -H "Authorization: Bearer $RELAY_WEBHOOK_TOKEN" \\
-H "Content-Type: application/json" \\
- -d '{"channel":"#alerts","text":"Deploy finished"}'`;
+ -d '{"message":"Deploy finished","author":"github-actions[bot]"}'`;
/**
* The "build the right context" band: real-time events, webhooks, and search.
diff --git a/web/content/docs/file/cli.mdx b/web/content/docs/file/cli.mdx
index 6104b5b..331b64f 100644
--- a/web/content/docs/file/cli.mdx
+++ b/web/content/docs/file/cli.mdx
@@ -17,7 +17,7 @@ The CLI resolves a token in priority order, first match wins:
For the cloud-hosted path, login is owned by the `agent-relay` CLI: `agent-relay login`, then `agent-relay workspace switch
`, then any `relayfile` command. For self-hosted servers, use the API-key path:
```bash
-relayfile login --api-key --server https://api.relayfile.dev
+relayfile login --api-key --server https://file.agentrelay.com
```
If no token is found, the CLI prints: `Error: not authenticated. Run 'agent-relay login' for Cloud or set RELAYFILE_TOKEN.`
@@ -49,7 +49,7 @@ Authenticate through the canonical relay session, or the self-hosted API-key pat
```bash
relayfile login [--no-open]
-relayfile login --api-key --server https://api.relayfile.dev
+relayfile login --api-key --server https://file.agentrelay.com
```
The default path delegates to `agent-relay login`. `--api-key` keeps the self-hosted compatibility path and writes `~/.relayfile/credentials.json` with `0600` permissions.
diff --git a/web/content/docs/file/introduction.mdx b/web/content/docs/file/introduction.mdx
index 287590e..7d50dcc 100644
--- a/web/content/docs/file/introduction.mdx
+++ b/web/content/docs/file/introduction.mdx
@@ -52,7 +52,7 @@ from relayfile import RelayFileClient
# The Python SDK is the data-plane client — connect a provider with the CLI
# above, then point the client at the workspace token to read and write:
client = RelayFileClient(
- "https://api.relayfile.dev",
+ "https://file.agentrelay.com",
lambda: os.environ["RELAYFILE_TOKEN"],
)
```
diff --git a/web/content/docs/file/python-sdk.mdx b/web/content/docs/file/python-sdk.mdx
index 13495a6..4695c9e 100644
--- a/web/content/docs/file/python-sdk.mdx
+++ b/web/content/docs/file/python-sdk.mdx
@@ -19,7 +19,7 @@ Construct a client with a base URL and a token (a string, or a callable that ret
from relayfile import RelayFileClient
client = RelayFileClient(
- "https://api.relayfile.dev",
+ "https://file.agentrelay.com",
lambda: os.environ["RELAYFILE_TOKEN"],
)
```
@@ -63,7 +63,7 @@ except RevisionConflictError:
```python
from relayfile import AsyncRelayFileClient
-async with AsyncRelayFileClient("https://api.relayfile.dev", token) as client:
+async with AsyncRelayFileClient("https://file.agentrelay.com", token) as client:
page = await client.read_file("rw_123", "/linear/issues/AGE-12__fix-login-bug.json")
```
diff --git a/web/content/docs/loop/sources.mdx b/web/content/docs/loop/sources.mdx
index bc96f1d..fd626b4 100644
--- a/web/content/docs/loop/sources.mdx
+++ b/web/content/docs/loop/sources.mdx
@@ -13,7 +13,7 @@ Relayloop's value comes from aggregating history that is otherwise scattered acr
| Codex CLI | `codex` | `~/.codex/history.jsonl` |
| Cursor | `cursor` | Per-session JSONL under `~/.cursor/projects/...` |
| Grok | `grok` | Per-session JSONL under `~/.grok/sessions/...` |
-| Agent Relay | `relay` | Relaycast API (`https://api.relaycast.dev/v1`) |
+| Agent Relay | `relay` | Relaycast API (`https://cast.agentrelay.com/v1`) |
| Trajectories | `trajectory` | Compacted per-run JSON files |
| OpenCode | `opencode` | Local SQLite (`~/.local/share/opencode/opencode.db`) |
diff --git a/web/content/docs/observer.mdx b/web/content/docs/observer.mdx
index 80db7e9..2ebc73a 100644
--- a/web/content/docs/observer.mdx
+++ b/web/content/docs/observer.mdx
@@ -74,7 +74,7 @@ Point the command at a different observer deployment with `--observer-url`, or
set `RELAY_OBSERVER_URL`:
```bash
-agent-relay observer --observer-url https://observer.relaycast.dev
+agent-relay observer --observer-url https://observer.agentrelay.com
```
## When to use it
diff --git a/web/lib/agent-discovery.ts b/web/lib/agent-discovery.ts
new file mode 100644
index 0000000..1a0c963
--- /dev/null
+++ b/web/lib/agent-discovery.ts
@@ -0,0 +1,143 @@
+// Machine-readable discovery documents served under /.well-known/.
+//
+// Agents that land on agentrelay.com without prior knowledge use these to find
+// the security contact, the HTTP APIs behind the product, and the MCP server
+// that exposes Relay as tools. Everything here points at surfaces that already
+// exist — the docs pages and the published OpenAPI specs — so the manifests
+// stay true without a separate source of truth to keep in sync.
+
+import { SITE_EMAIL, SITE_URL, absoluteUrl } from './site';
+
+const RELAYCAST_OPENAPI_URL =
+ 'https://raw.githubusercontent.com/AgentWorkforce/relaycast/main/openapi.yaml';
+const RELAYFILE_OPENAPI_URL =
+ 'https://raw.githubusercontent.com/AgentWorkforce/relayfile/main/openapi/relayfile-v1.openapi.yaml';
+
+/** RFC 9116 requires an absolute, machine-parseable expiry no more than a year out. */
+export function securityTxtExpiry(now: Date): string {
+ const expires = new Date(now);
+ expires.setUTCFullYear(expires.getUTCFullYear() + 1);
+ expires.setUTCMilliseconds(0);
+ return expires.toISOString().replace('.000Z', 'Z');
+}
+
+/** RFC 9116 security.txt. */
+export function getSecurityTxt(now: Date): string {
+ return [
+ '# Agent Relay security contact (RFC 9116).',
+ '# Report a vulnerability by email; we acknowledge within two business days.',
+ '',
+ `Contact: mailto:${SITE_EMAIL}`,
+ `Expires: ${securityTxtExpiry(now)}`,
+ 'Preferred-Languages: en',
+ `Canonical: ${absoluteUrl('/.well-known/security.txt')}`,
+ `Policy: ${absoluteUrl('/terms')}`,
+ '',
+ ].join('\n');
+}
+
+type LinksetLink = { href: string; type: string; title: string };
+
+type LinksetEntry = {
+ anchor: string;
+ 'service-desc'?: LinksetLink[];
+ 'service-doc'?: LinksetLink[];
+ 'service-meta'?: LinksetLink[];
+};
+
+/**
+ * RFC 9727 API catalog: one linkset entry per HTTP API, each pointing at its
+ * OpenAPI description (`service-desc`) and its human documentation
+ * (`service-doc`).
+ */
+export function getApiCatalog(): { linkset: LinksetEntry[] } {
+ return {
+ linkset: [
+ {
+ anchor: 'https://cast.agentrelay.com/v1',
+ 'service-desc': [
+ {
+ href: RELAYCAST_OPENAPI_URL,
+ type: 'application/yaml',
+ title: 'Relaycast v1 OpenAPI 3.0 description',
+ },
+ ],
+ 'service-doc': [
+ {
+ href: absoluteUrl('/docs/relaycast-api'),
+ type: 'text/html',
+ title: 'Relaycast API reference',
+ },
+ {
+ href: absoluteUrl('/docs/markdown/relaycast-api.md'),
+ type: 'text/markdown',
+ title: 'Relaycast API reference (markdown)',
+ },
+ ],
+ 'service-meta': [
+ {
+ href: absoluteUrl('/docs/authentication'),
+ type: 'text/html',
+ title: 'Authentication: token types and scopes',
+ },
+ ],
+ },
+ {
+ anchor: 'https://file.agentrelay.com/v1',
+ 'service-desc': [
+ {
+ href: RELAYFILE_OPENAPI_URL,
+ type: 'application/yaml',
+ title: 'Relayfile v1 OpenAPI description',
+ },
+ ],
+ 'service-doc': [
+ {
+ href: absoluteUrl('/docs/file/api-reference'),
+ type: 'text/html',
+ title: 'Relayfile API reference',
+ },
+ ],
+ },
+ ],
+ };
+}
+
+/**
+ * MCP server manifest. `agent-relay mcp` is a stdio server shipped in the CLI,
+ * so the manifest describes how to launch it rather than a remote URL.
+ */
+export function getMcpManifest() {
+ return {
+ name: 'agent-relay',
+ description:
+ 'Agent Relay messaging as MCP tools: channels, DMs, threads, reactions, inbox, and Zod-backed actions.',
+ documentation: absoluteUrl('/docs/agent-relay-mcp'),
+ website: SITE_URL,
+ servers: [
+ {
+ name: 'agent-relay',
+ transport: 'stdio',
+ command: 'agent-relay',
+ args: ['mcp'],
+ install: {
+ registry: 'npm',
+ package: 'agent-relay',
+ command: 'npm install -g agent-relay',
+ },
+ env: [
+ {
+ name: 'RELAY_WORKSPACE_KEY',
+ description: 'Workspace join secret (rk_live_*). Create one with `agent-relay workspace create`.',
+ required: true,
+ },
+ {
+ name: 'RELAY_BASE_URL',
+ description: 'API base URL override, for self-hosted @relaycast/engine deployments.',
+ required: false,
+ },
+ ],
+ },
+ ],
+ };
+}
diff --git a/web/lib/test/agent-discovery.test.ts b/web/lib/test/agent-discovery.test.ts
new file mode 100644
index 0000000..89c4771
--- /dev/null
+++ b/web/lib/test/agent-discovery.test.ts
@@ -0,0 +1,49 @@
+import { describe, expect, it } from 'vitest';
+
+import { getApiCatalog, getMcpManifest, getSecurityTxt, securityTxtExpiry } from '../agent-discovery';
+
+describe('security.txt', () => {
+ it('carries the RFC 9116 required fields', () => {
+ const txt = getSecurityTxt(new Date('2026-08-21T00:00:00Z'));
+
+ expect(txt).toContain('Contact: mailto:hello@agentrelay.com');
+ expect(txt).toContain('Expires: 2027-08-21T00:00:00Z');
+ expect(txt).toContain('Canonical: https://agentrelay.com/.well-known/security.txt');
+ expect(txt).toContain('Preferred-Languages: en');
+ });
+
+ it('always expires in the future', () => {
+ const now = new Date('2026-08-21T00:00:00Z');
+ expect(Date.parse(securityTxtExpiry(now))).toBeGreaterThan(now.getTime());
+ });
+});
+
+describe('api-catalog', () => {
+ it('links each API to an OpenAPI description and human docs', () => {
+ const { linkset } = getApiCatalog();
+
+ // Anchors must be API hosts that actually exist — api.agentrelay.com does not.
+ expect(linkset.map((e) => e.anchor)).toEqual([
+ 'https://cast.agentrelay.com/v1',
+ 'https://file.agentrelay.com/v1',
+ ]);
+ for (const entry of linkset) {
+ expect(entry.anchor).toMatch(/^https:\/\//);
+ expect(entry['service-desc']?.[0]?.href).toMatch(/^https:\/\//);
+ expect(entry['service-doc']?.[0]?.href).toMatch(/^https:\/\/agentrelay\.com\//);
+ }
+ });
+});
+
+describe('mcp.json', () => {
+ it('describes the stdio server well enough to launch it', () => {
+ const manifest = getMcpManifest();
+ const [server] = manifest.servers;
+
+ expect(manifest.documentation).toBe('https://agentrelay.com/docs/agent-relay-mcp');
+ expect(server.transport).toBe('stdio');
+ expect(server.command).toBe('agent-relay');
+ expect(server.args).toEqual(['mcp']);
+ expect(server.env.find((e) => e.name === 'RELAY_WORKSPACE_KEY')?.required).toBe(true);
+ });
+});
diff --git a/web/next.config.mjs b/web/next.config.mjs
index 08fe1c1..fdf4431 100644
--- a/web/next.config.mjs
+++ b/web/next.config.mjs
@@ -31,11 +31,59 @@ const nextConfig = {
return config;
},
+ async headers() {
+ // Machine-readable surfaces an agent may fetch from another origin (or from
+ // a browser-based agent). Without CORS these are unreadable to anything that
+ // isn't a server-side crawler.
+ const agentReadable = [
+ '/llms.txt',
+ '/llms-full.txt',
+ '/llm.txt',
+ '/agents.md',
+ '/skill.md',
+ '/feed.xml',
+ '/sitemap.xml',
+ '/robots.txt',
+ '/docs/llms.txt',
+ '/docs/markdown.md',
+ '/docs/markdown/:path*',
+ '/docs/agents/markdown/:path*',
+ '/docs/factory/markdown/:path*',
+ '/docs/file/markdown/:path*',
+ '/docs/loop/markdown/:path*',
+ '/docs/:slug([^/]+\\.md)',
+ '/.well-known/:path*',
+ ];
+
+ return [
+ {
+ source: '/:path*',
+ headers: [{ key: 'X-Content-Type-Options', value: 'nosniff' }],
+ },
+ ...agentReadable.map((source) => ({
+ source,
+ headers: [
+ { key: 'Access-Control-Allow-Origin', value: '*' },
+ { key: 'Access-Control-Allow-Methods', value: 'GET, HEAD' },
+ ],
+ })),
+ {
+ // The internal path the /.well-known rewrite targets; keep it out of
+ // indexes so the canonical dot-prefixed URL is the only one advertised.
+ source: '/well-known/:path*',
+ headers: [{ key: 'X-Robots-Tag', value: 'noindex' }],
+ },
+ ];
+ },
async rewrites() {
return {
afterFiles: [
// Conventional llms.txt path under /docs resolves to the root route.
{ source: '/docs/llms.txt', destination: '/llms.txt' },
+ // The App Router skips dot-prefixed directories, so the /.well-known
+ // documents are implemented under app/well-known/ and surfaced here at
+ // their canonical paths.
+ { source: '/.well-known/:path*', destination: '/well-known/:path*' },
// Append .md to any docs URL to get its markdown mirror. afterFiles
// runs after static routes (so /docs/markdown.md is untouched) but
// before the /docs/[slug] dynamic page.