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
12 changes: 12 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ const wellKnownSkills = {
pattern: '/.well-known/agent-skills/[name]/[file]',
entrypoint: './src/wellknown/skill.ts',
});
injectRoute({
pattern: '/.well-known/api-catalog',
entrypoint: './src/wellknown/api-catalog.ts',
});
injectRoute({
pattern: '/.well-known/openapi.json',
entrypoint: './src/wellknown/openapi.ts',
});
injectRoute({
pattern: '/.well-known/mcp/server-card.json',
entrypoint: './src/wellknown/mcp-server-card.ts',
});
},
},
};
Expand Down
1 change: 1 addition & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
User-agent: *
Allow: /
Content-Signal: ai-train=no, search=yes, ai-input=no

Sitemap: https://sembr.org/sitemap-index.xml
1 change: 1 addition & 0 deletions src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,6 @@ const structuredData = {
<main>
<slot />
</main>
<script src="../scripts/webmcp.ts"></script>
</body>
</html>
222 changes: 222 additions & 0 deletions src/lib/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
export const SITE_ORIGIN = 'https://sembr.org';

export const DISCOVERY_PATHS = {
apiCatalog: '/.well-known/api-catalog',
openApi: '/.well-known/openapi.json',
mcpServerCard: '/.well-known/mcp/server-card.json',
agentSkillsIndex: '/.well-known/agent-skills/index.json',
llmsTxt: '/llms.txt',
health: '/health',
sitemap: '/sitemap-index.xml',
} as const;

export function discoveryLinkHeader(): string {
return [
`</${DISCOVERY_PATHS.sitemap.slice(1)}>; rel="sitemap"`,
`</${DISCOVERY_PATHS.apiCatalog.slice(1)}>; rel="api-catalog"`,
`</${DISCOVERY_PATHS.openApi.slice(1)}>; rel="service-desc"; type="application/vnd.oai.openapi+json"`,
`</${DISCOVERY_PATHS.llmsTxt.slice(1)}>; rel="describedby"; type="text/markdown"`,
`</${DISCOVERY_PATHS.mcpServerCard.slice(1)}>; rel="describedby"; type="application/json"`,
].join(', ');
}

export function apiCatalogLinkset() {
const origin = SITE_ORIGIN;

return {
linkset: [
{
anchor: `${origin}/`,
'service-desc': [
{
href: `${origin}${DISCOVERY_PATHS.openApi}`,
type: 'application/vnd.oai.openapi+json',
},
],
'service-doc': [
{
href: `${origin}${DISCOVERY_PATHS.llmsTxt}`,
type: 'text/markdown',
},
{
href: `${origin}/`,
type: 'text/html',
},
],
status: [
{
href: `${origin}${DISCOVERY_PATHS.health}`,
type: 'application/json',
},
],
},
{
anchor: `${origin}/.well-known/agent-skills/`,
'service-desc': [
{
href: `${origin}${DISCOVERY_PATHS.openApi}#/paths/~1.well-known~1agent-skills~1index.json/get`,
type: 'application/vnd.oai.openapi+json',
},
],
'service-doc': [
{
href: `${origin}${DISCOVERY_PATHS.agentSkillsIndex}`,
type: 'application/json',
},
],
status: [
{
href: `${origin}${DISCOVERY_PATHS.health}`,
type: 'application/json',
},
],
},
],
};
}

export function openApiDocument() {
const origin = SITE_ORIGIN;

return {
openapi: '3.1.0',
info: {
title: 'SemBr.org Content API',
version: '1.0.0',
description:
'Machine-readable access to the Semantic Line Breaks specification and published agent skills.',
},
servers: [{ url: origin }],
paths: {
'/': {
get: {
summary: 'SemBr specification',
description:
'Returns the SemBr specification. Use Accept: text/markdown for the source document or Accept: text/html for the rendered page.',
parameters: [
{
name: 'Accept',
in: 'header',
schema: {
type: 'string',
enum: ['text/markdown', 'text/html'],
},
},
Comment thread
mattt marked this conversation as resolved.
],
responses: {
'200': {
description: 'SemBr specification document',
content: {
'text/markdown': {},
'text/html': {},
},
},
},
},
},
'/llms.txt': {
get: {
summary: 'SemBr specification (llms.txt)',
description: 'Returns the SemBr specification as Markdown.',
responses: {
'200': {
description: 'SemBr specification',
content: {
'text/markdown': {},
},
},
},
},
},
'/.well-known/agent-skills/index.json': {
get: {
summary: 'Agent skills index',
description: 'Lists SemBr agent skills available for discovery.',
responses: {
'200': {
description: 'Agent skills discovery document',
content: {
'application/json': {},
},
},
},
},
},
'/.well-known/agent-skills/{name}/SKILL.md': {
get: {
summary: 'Agent skill document',
parameters: [
{
name: 'name',
in: 'path',
required: true,
schema: { type: 'string' },
},
],
responses: {
'200': {
description: 'Agent skill Markdown document',
content: {
'text/markdown': {},
},
},
'404': {
description: 'Skill not found',
},
},
},
},
'/health': {
get: {
summary: 'Service health',
responses: {
'200': {
description: 'Health status',
content: {
'application/json': {},
},
},
},
},
},
},
};
}

export function mcpServerCard() {
return {
protocolVersion: '2025-11-25',
serverInfo: {
name: 'sembr.org',
version: '0.0.1',
description:
'Semantic Line Breaks specification, llms.txt, and published agent skills exposed via WebMCP on the homepage.',
},
transport: {
type: 'webmcp',
endpoint: `${SITE_ORIGIN}/`,
},
capabilities: {
tools: true,
resources: false,
prompts: false,
},
authentication: {
required: false,
},
tools: [
{
name: 'get_specification',
description: 'Fetch the SemBr specification as Markdown from /llms.txt.',
},
{
name: 'list_agent_skills',
description: 'List published SemBr agent skills from the well-known index.',
},
{
name: 'get_agent_skill',
description: 'Fetch a specific SemBr agent skill by name.',
},
],
};
}
11 changes: 11 additions & 0 deletions src/pages/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { APIRoute } from 'astro';

export const prerender = true;

export const GET: APIRoute = () =>
new Response(JSON.stringify({ status: 'ok' }) + '\n', {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-cache',
},
});
5 changes: 5 additions & 0 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export const prerender = false;

import { getEntry, render } from "astro:content";
import { discoveryLinkHeader } from "../lib/discovery";
import Layout from "../layouts/Layout.astro";

const entry = await getEntry("specification", "sembr");
Expand All @@ -10,15 +11,19 @@ if (!entry) {
throw new Error("SemBr specification not available.");
}

const linkHeader = discoveryLinkHeader();

if (!Astro.request.headers.get("accept")?.includes("text/html")) {
return new Response(entry.body, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
Link: linkHeader,
Vary: "Accept",
},
});
}

Astro.response.headers.set("Link", linkHeader);
Astro.response.headers.set("Vary", "Accept");

const { Content } = await render(entry);
Expand Down
Loading