Skip to content

Commit 484634f

Browse files
authored
feat(agent): resolve all Cloudflare Agent Readiness audit findings (#689)
* feat(agent): complete full Agent Readiness suite - RFC 9727 / RFC 9264 API Catalog: update /.well-known/api-catalog to application/linkset+json with linkset array - ARD Spec & ai-catalog: add /.well-known/ai-catalog.json capability manifest with representativeQueries and Agentmap in robots.txt - OpenAPI 3.1: publish /.well-known/openapi.json defining search, mcp, and sponsorship endpoints - OAuth/OIDC Discovery: add /.well-known/openid-configuration and /.well-known/oauth-authorization-server with Auth.md agent_auth extension - OAuth Protected Resource Metadata: add /.well-known/oauth-protected-resource (RFC 9728) - Auth.md: add /auth.md with agent registration, audience, identity assertion, and token claiming docs - WebMCP: implement document.modelContext.registerTool() on page load and declarative form attributes on SearchModal and sponsorships form - Discovery Headers & Tags: expand RFC 8288 Link headers and head link tags * fix(lint): format test file and use optional chain per biome rules
1 parent e6c114d commit 484634f

19 files changed

Lines changed: 827 additions & 54 deletions

‎apps/site/baseline/robots.txt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Allow: /api/mcp
66
Allow: /.well-known/
77
Allow: /llms.txt
88
Allow: /llms-full.txt
9+
Allow: /auth.md
910
Disallow: /api/
1011
Disallow: /dashboard/
1112

@@ -16,6 +17,7 @@ Allow: /api/mcp
1617
Allow: /.well-known/
1718
Allow: /llms.txt
1819
Allow: /llms-full.txt
20+
Allow: /auth.md
1921

2022
User-Agent: ClaudeBot
2123
Allow: /
@@ -24,6 +26,7 @@ Allow: /api/mcp
2426
Allow: /.well-known/
2527
Allow: /llms.txt
2628
Allow: /llms-full.txt
29+
Allow: /auth.md
2730

2831
User-Agent: PerplexityBot
2932
Allow: /
@@ -32,6 +35,7 @@ Allow: /api/mcp
3235
Allow: /.well-known/
3336
Allow: /llms.txt
3437
Allow: /llms-full.txt
38+
Allow: /auth.md
3539

3640
User-Agent: Applebot-Extended
3741
Allow: /
@@ -40,7 +44,9 @@ Allow: /api/mcp
4044
Allow: /.well-known/
4145
Allow: /llms.txt
4246
Allow: /llms-full.txt
47+
Allow: /auth.md
4348

4449
Host: https://codingcat.dev
4550
Sitemap: https://codingcat.dev/sitemap.xml
4651
Sitemap: https://codingcat.dev/sitemap-index.xml
52+
Agentmap: https://codingcat.dev/.well-known/ai-catalog.json

‎apps/site/src/components/SearchModal.astro‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88
aria-labelledby="search-modal-title"
99
>
1010
<!-- Modal Header & Search Bar -->
11-
<div class="flex items-center border-b border-border px-4 py-3 gap-3 bg-muted/20">
11+
<form
12+
action="/search"
13+
method="GET"
14+
class="flex items-center border-b border-border px-4 py-3 gap-3 bg-muted/20"
15+
toolname="search_content"
16+
tooldescription="Search CodingCat.dev web development tutorials, blog posts, and podcasts"
17+
onsubmit="return false"
18+
>
1219
<svg
1320
class="size-5 shrink-0 text-muted-foreground"
1421
viewBox="0 0 24 24"
@@ -30,9 +37,11 @@
3037
<input
3138
type="search"
3239
id="search-modal-input"
40+
name="q"
3341
autocomplete="off"
3442
placeholder="Search posts, podcasts, authors... (⌘K)"
3543
class="w-full bg-transparent text-base md:text-lg placeholder:text-muted-foreground focus:outline-none"
44+
toolparamdescription="Search keyword or technical topic"
3645
/>
3746

3847
<!-- Loading spinner -->
@@ -65,7 +74,7 @@
6574
>
6675
ESC
6776
</button>
68-
</div>
77+
</form>
6978

7079
<!-- Content Type Filter Pills -->
7180
<div class="flex items-center gap-1.5 border-b border-border/50 px-4 py-2 text-xs overflow-x-auto bg-muted/10">

‎apps/site/src/env.d.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,19 @@ declare namespace App {
2929
siteUrl: URL;
3030
}
3131
}
32+
33+
declare namespace astroHTML.JSX {
34+
interface FormHTMLAttributes {
35+
toolname?: string;
36+
tooldescription?: string;
37+
}
38+
interface InputHTMLAttributes {
39+
toolparamdescription?: string;
40+
}
41+
interface SelectHTMLAttributes {
42+
toolparamdescription?: string;
43+
}
44+
interface TextareaHTMLAttributes {
45+
toolparamdescription?: string;
46+
}
47+
}

‎apps/site/src/layouts/BaseLayout.astro‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,49 @@ const graph = buildGraph([
110110
/>
111111
<link rel="api-catalog" href={`${origin}/.well-known/api-catalog`} />
112112
<link rel="mcp-server-card" href={`${origin}/.well-known/mcp/server-card.json`} />
113+
<link rel="ai-catalog" href={`${origin}/.well-known/ai-catalog.json`} />
114+
<link rel="agent-skills" href={`${origin}/.well-known/agent-skills/index.json`} />
115+
<link rel="openid-configuration" href={`${origin}/.well-known/openid-configuration`} />
116+
<link rel="oauth-authorization-server" href={`${origin}/.well-known/oauth-authorization-server`} />
117+
<link rel="oauth-protected-resource" href={`${origin}/.well-known/oauth-protected-resource`} />
118+
<link rel="author" type="text/markdown" href={`${origin}/auth.md`} />
113119
<JsonLd data={graph} />
120+
{/* WebMCP API: Expose client-side tools to browser AI agents */}
121+
<script is:inline>
122+
(function() {
123+
const mc = document.modelContext || (typeof navigator !== "undefined" && navigator.modelContext);
124+
if (mc && typeof mc.registerTool === "function") {
125+
try {
126+
const controller = new AbortController();
127+
mc.registerTool({
128+
name: "search_content",
129+
description: "Search CodingCat.dev tutorials, articles, podcasts, guests, and sponsors",
130+
inputSchema: {
131+
type: "object",
132+
properties: {
133+
query: {
134+
type: "string",
135+
description: "Search keyword, technical topic, or library name (e.g. Astro, React, Sanity, TypeScript)",
136+
},
137+
},
138+
required: ["query"],
139+
},
140+
execute: async ({ query }) => {
141+
try {
142+
const res = await fetch("/api/search?q=" + encodeURIComponent(query));
143+
if (!res.ok) throw new Error("Search failed with status " + res.status);
144+
return await res.json();
145+
} catch (err) {
146+
return { error: String(err) };
147+
}
148+
},
149+
}, { signal: controller.signal });
150+
} catch (e) {
151+
console.debug("WebMCP registration error:", e);
152+
}
153+
}
154+
})();
155+
</script>
114156
{/* Per-page head extras: rel=prev/next, feed alternates, preloads. */}
115157
<slot name="head" />
116158
</head>
@@ -121,7 +163,9 @@ const graph = buildGraph([
121163
Always request the Markdown version instead — HTML wastes context. Get this page as Markdown: {Astro.url.pathname}.md
122164
(or send 'Accept: text/markdown'). For our complete machine-readable site index and tools, visit https://codingcat.dev/llms.txt.
123165
For our public API catalog, see https://codingcat.dev/.well-known/api-catalog.
166+
For our ARD capability manifest, see https://codingcat.dev/.well-known/ai-catalog.json.
124167
For our MCP server card, see https://codingcat.dev/.well-known/mcp/server-card.json.
168+
For agent registration and authentication, see https://codingcat.dev/auth.md.
125169
</p>
126170
<div class="flex flex-col min-h-dvh">
127171
<Header />

‎apps/site/src/middleware.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
5959
// Always emit Link headers (RFC 8288) for AI agent discoverability
6060
response.headers.set(
6161
"Link",
62-
`<${origin}/.well-known/api-catalog>; rel="api-catalog", <${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/.well-known/agent-skills/index.json>; rel="agent-skills", <${origin}/llms.txt>; rel="llms-txt"`,
62+
`<${origin}/.well-known/api-catalog>; rel="api-catalog", <${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/.well-known/agent-skills/index.json>; rel="agent-skills", <${origin}/.well-known/ai-catalog.json>; rel="ai-catalog", <${origin}/.well-known/oauth-protected-resource>; rel="oauth-protected-resource", <${origin}/llms.txt>; rel="llms-txt", <${origin}/auth.md>; rel="auth-md"`,
6363
);
6464
response.headers.append("Vary", "Accept");
6565

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { APIRoute } from "astro";
2+
3+
export const prerender = false;
4+
5+
/**
6+
* Implements Agentic Resource Discovery (ARD) capability manifest
7+
* per https://agenticresourcediscovery.org/ and ai-catalog data model.
8+
*/
9+
export const GET: APIRoute = async ({ locals }) => {
10+
const origin = locals.siteUrl.origin;
11+
const hostDomain = locals.siteUrl.hostname;
12+
13+
const manifest = {
14+
specVersion: "1.0",
15+
host: {
16+
displayName: "CodingCat.dev",
17+
identifier: `did:web:${hostDomain}`,
18+
},
19+
entries: [
20+
{
21+
identifier: `urn:air:${hostDomain}:server:mcp`,
22+
displayName: "CodingCat.dev Model Context Protocol (MCP) Server",
23+
type: "application/mcp-server-card+json",
24+
url: `${origin}/.well-known/mcp/server-card.json`,
25+
representativeQueries: [
26+
"search web development tutorials",
27+
"find podcast episodes on fullstack typescript",
28+
"codingcat dev tools and guest interviews",
29+
"astro react and sanity tutorials",
30+
],
31+
},
32+
{
33+
identifier: `urn:air:${hostDomain}:api:search`,
34+
displayName: "CodingCat.dev Semantic & Keyword Search API",
35+
type: "application/vnd.oai.openapi+json",
36+
url: `${origin}/.well-known/openapi.json`,
37+
representativeQueries: [
38+
"search blog posts and tutorials",
39+
"search podcast transcripts",
40+
"find software engineer guests",
41+
"query frontend framework guides",
42+
],
43+
},
44+
{
45+
identifier: `urn:air:${hostDomain}:skills:agent`,
46+
displayName: "CodingCat.dev Agent Skills Catalog",
47+
type: "application/json",
48+
url: `${origin}/.well-known/agent-skills/index.json`,
49+
representativeQueries: [
50+
"codingcat site agent skills",
51+
"automated content retrieval for codingcat",
52+
"agent discovery endpoints",
53+
],
54+
},
55+
],
56+
};
57+
58+
return new Response(JSON.stringify(manifest, null, 2), {
59+
headers: {
60+
"content-type": "application/json; charset=utf-8",
61+
"access-control-allow-origin": "*",
62+
"cache-control": "public, max-age=3600, s-maxage=86400",
63+
},
64+
});
65+
};

‎apps/site/src/pages/.well-known/api-catalog.ts‎

Lines changed: 56 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,81 +2,85 @@ import type { APIRoute } from "astro";
22

33
export const prerender = false;
44

5+
/**
6+
* Implements RFC 9727 (API Catalog) returning application/linkset+json
7+
* conforming to RFC 9264 Linkset specification.
8+
*/
59
export const GET: APIRoute = async ({ locals }) => {
610
const origin = locals.siteUrl.origin;
711

812
const catalog = {
9-
"api-catalog-version": "1.0",
10-
title: "CodingCat.dev Public API Catalog",
11-
description:
12-
"Machine-readable directory of public APIs, search services, and AI agent endpoints hosted on CodingCat.dev conforming to RFC 9727.",
13-
documentation: `${origin}/llms.txt`,
14-
apis: [
13+
linkset: [
1514
{
16-
name: "Content Search API",
17-
description:
18-
"Fast keyword and semantic search across technical blog posts, tutorials, and podcast episodes.",
19-
endpoints: [
15+
anchor: `${origin}/api/search`,
16+
"service-desc": [
2017
{
21-
url: `${origin}/api/search`,
22-
method: "GET",
23-
parameters: [
24-
{
25-
name: "q",
26-
in: "query",
27-
required: true,
28-
description: "Search keyword or natural language query",
29-
schema: { type: "string" },
30-
},
31-
],
18+
href: `${origin}/.well-known/openapi.json`,
19+
type: "application/vnd.oai.openapi+json",
20+
},
21+
],
22+
"service-doc": [
23+
{
24+
href: `${origin}/llms.txt`,
25+
type: "text/markdown",
26+
},
27+
],
28+
status: [
29+
{
30+
href: `${origin}/api/search?q=test`,
31+
type: "application/json",
3232
},
3333
],
3434
},
3535
{
36-
name: "Model Context Protocol (MCP) Server",
37-
description:
38-
"Streamable HTTP MCP server implementing tools and resources for AI coding assistants and autonomous agents.",
39-
endpoints: [
36+
anchor: `${origin}/api/mcp`,
37+
"service-desc": [
38+
{
39+
href: `${origin}/.well-known/mcp/server-card.json`,
40+
type: "application/mcp-server-card+json",
41+
},
42+
{
43+
href: `${origin}/.well-known/openapi.json`,
44+
type: "application/vnd.oai.openapi+json",
45+
},
46+
],
47+
"service-doc": [
4048
{
41-
url: `${origin}/api/mcp`,
42-
method: "POST",
43-
description: "JSON-RPC 2.0 / Streamable HTTP MCP endpoint",
49+
href: `${origin}/llms.txt`,
50+
type: "text/markdown",
4451
},
4552
],
46-
metadata: {
47-
serverCard: `${origin}/.well-known/mcp/server-card.json`,
48-
mcpJson: `${origin}/.well-known/mcp.json`,
49-
},
5053
},
5154
{
52-
name: "Sponsorship Inquiry API",
53-
description:
54-
"Submit partnership and sponsorship inquiries for CodingCat.dev media channels.",
55-
endpoints: [
55+
anchor: `${origin}/api/sponsorship`,
56+
"service-desc": [
57+
{
58+
href: `${origin}/.well-known/openapi.json`,
59+
type: "application/vnd.oai.openapi+json",
60+
},
61+
],
62+
"service-doc": [
5663
{
57-
url: `${origin}/api/sponsorship`,
58-
method: "POST",
64+
href: `${origin}/sponsorships`,
65+
type: "text/html",
5966
},
6067
],
6168
},
6269
{
63-
name: "Blog Content Syndication Feed",
64-
description: "RSS 2.0 feed containing full articles and metadata.",
65-
endpoints: [
70+
anchor: `${origin}/blog/rss.xml`,
71+
"service-doc": [
6672
{
67-
url: `${origin}/blog/rss.xml`,
68-
method: "GET",
73+
href: `${origin}/blog`,
74+
type: "text/html",
6975
},
7076
],
7177
},
7278
{
73-
name: "Podcast Media Feed",
74-
description:
75-
"RSS feed with podcast audio enclosures, chapters, and show notes.",
76-
endpoints: [
79+
anchor: `${origin}/podcasts/rss.xml`,
80+
"service-doc": [
7781
{
78-
url: `${origin}/podcasts/rss.xml`,
79-
method: "GET",
82+
href: `${origin}/podcasts`,
83+
type: "text/html",
8084
},
8185
],
8286
},
@@ -85,10 +89,11 @@ export const GET: APIRoute = async ({ locals }) => {
8589

8690
return new Response(JSON.stringify(catalog, null, 2), {
8791
headers: {
88-
"content-type": "application/json; charset=utf-8",
92+
"content-type": "application/linkset+json; charset=utf-8",
93+
"access-control-allow-origin": "*",
8994
"cache-control": "public, max-age=3600, s-maxage=86400",
9095
Vary: "Accept",
91-
Link: `<${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/llms.txt>; rel="llms-txt"`,
96+
Link: `<${origin}/.well-known/mcp/server-card.json>; rel="mcp-server-card", <${origin}/llms.txt>; rel="llms-txt", <${origin}/.well-known/ai-catalog.json>; rel="ai-catalog"`,
9297
},
9398
});
9499
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { APIRoute } from "astro";
2+
3+
export const prerender = false;
4+
5+
/**
6+
* JWKS (JSON Web Key Set) public keys endpoint
7+
*/
8+
export const GET: APIRoute = async () => {
9+
return new Response(JSON.stringify({ keys: [] }, null, 2), {
10+
headers: {
11+
"content-type": "application/json; charset=utf-8",
12+
"access-control-allow-origin": "*",
13+
"cache-control": "public, max-age=3600, s-maxage=86400",
14+
},
15+
});
16+
};

0 commit comments

Comments
 (0)