diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff229d761..2490f1deb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,6 +132,9 @@ jobs: - name: Test tracking redirect middleware run: npm run test:tracking-redirect + - name: Test agent discovery catalog + run: npm run test:agent-discovery + - name: Build site run: npm run build diff --git a/package.json b/package.json index 18983cd39..297a79792 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:heading-tocs": "node scripts/validate-heading-tocs.mjs", "test:heading-vars": "node --test src/plugins/heading-vars-core.test.mjs", "test:tracking-redirect": "node --test scripts/tracking-redirect.test.mjs", + "test:agent-discovery": "node --test scripts/agent-discovery.test.mjs", "test:tracking-routes": "node scripts/validate-tracking-routes.mjs", "test:kapa-webmcp": "node --test src/lib/kapa-webmcp.test.mjs", "test:kapa-support-escalation": "node --test src/lib/kapa-support-escalation.test.mjs", diff --git a/public/.well-known/ard.json b/public/.well-known/ard.json new file mode 100644 index 000000000..683a81dca --- /dev/null +++ b/public/.well-known/ard.json @@ -0,0 +1,30 @@ +{ + "entries": [ + { + "identifier": "urn:air:warp.dev:server:documentation", + "displayName": "Warp Documentation MCP Server", + "type": "application/mcp-server-card+json", + "url": "https://docs.warp.dev/.well-known/mcp.json", + "capabilities": ["Search Warp documentation", "Retrieve Warp documentation"], + "description": "Search and retrieve content from Warp's official documentation", + "representativeQueries": ["How do I use Warp agents?", "How do I use Warp Factories?"] + }, + { + "identifier": "urn:air:warp.dev:api:agent", + "displayName": "Warp Agent API", + "type": "application/openapi+json;version=3.2", + "url": "https://docs.warp.dev/openapi.json", + "capabilities": ["Create cloud agent runs", "Inspect cloud agent runs"], + "description": "OpenAPI description for programmatically creating, managing, and querying Warp cloud agent runs.", + "representativeQueries": ["Create a Warp cloud agent run", "Check the status of a Warp agent run"] + }, + { + "identifier": "urn:air:warp.dev:resource:docs-llms", + "displayName": "Warp Documentation LLM Index", + "type": "text/plain", + "url": "https://docs.warp.dev/llms.txt", + "description": "Machine-readable index of the Warp documentation, with links to the full, abridged, and per-section text sets.", + "representativeQueries": ["Find the Warp documentation for AI agents", "Get the full Warp documentation as plain text"] + } + ] +} diff --git a/public/.well-known/mcp.json b/public/.well-known/mcp.json index a52298503..612d432aa 100644 --- a/public/.well-known/mcp.json +++ b/public/.well-known/mcp.json @@ -1,5 +1,5 @@ { "name": "Warp Documentation", - "description": "Search and retrieve Warp documentation — the agentic development environment and Oz, Warp's programmable agent platform.", + "description": "Search and retrieve content from Warp's official documentation", "url": "https://warp.mcp.kapa.ai" } diff --git a/scripts/agent-discovery.test.mjs b/scripts/agent-discovery.test.mjs new file mode 100644 index 000000000..cd679ab68 --- /dev/null +++ b/scripts/agent-discovery.test.mjs @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { test } from 'node:test'; + +const DOCS_ORIGIN = 'https://docs.warp.dev'; +const repoFile = (path) => new URL(`../${path}`, import.meta.url); +const readJson = async (path) => JSON.parse(await readFile(repoFile(path), 'utf8')); + +const catalog = await readJson('public/.well-known/ard.json'); +const byId = (identifier) => catalog.entries.find((entry) => entry.identifier === identifier); + +test('ARD catalog entries are valid', () => { + // ARD requires only `entries` at the root; other root members are allowed. + assert.ok(Array.isArray(catalog.entries) && catalog.entries.length > 0); + const identifiers = catalog.entries.map((entry) => entry.identifier); + assert.equal(new Set(identifiers).size, identifiers.length, 'Identifiers must be unique'); + for (const entry of catalog.entries) { + assert.match(entry.identifier, /^urn:air:warp\.dev:[a-z0-9._-]+:[a-z0-9._-]+$/); + assert.equal(typeof entry.displayName, 'string'); + assert.equal(typeof entry.type, 'string'); + assert.equal(typeof entry.description, 'string'); + assert.notEqual('url' in entry, 'data' in entry, `${entry.identifier} needs exactly one of url or data`); + assert.ok(entry.url.startsWith('https://'), `${entry.identifier} url must use HTTPS`); + assert.ok(entry.representativeQueries.length >= 2 && entry.representativeQueries.length <= 5); + for (const query of entry.representativeQueries) assert.equal(typeof query, 'string'); + } +}); + +test('MCP entry points to the served MCP manifest', async () => { + const entry = byId('urn:air:warp.dev:server:documentation'); + assert.equal(entry.url, `${DOCS_ORIGIN}/.well-known/mcp.json`); + // ARD v0.91 (ADR-0008) replaced application/mcp-server+json with the server-card type. + assert.equal(entry.type, 'application/mcp-server-card+json'); + const manifest = await readJson('public/.well-known/mcp.json'); + assert.match(manifest.url, /^https:\/\//); +}); + +test('OpenAPI entry media type matches the spec version', async () => { + const entry = byId('urn:air:warp.dev:api:agent'); + assert.equal(entry.url, `${DOCS_ORIGIN}/openapi.json`); + const spec = await readFile(repoFile('developers/agent-api-openapi.yaml'), 'utf8'); + const [, major, minor] = spec.match(/^openapi:\s*['"]?(\d+)\.(\d+)/m); + // application/openapi+json is the registered type; application/vnd.oai.openapi+json is a deprecated alias. + assert.equal(entry.type, `application/openapi+json;version=${major}.${minor}`); +}); + +test('LLM index entry points to the generated docs llms.txt', async () => { + const entry = byId('urn:air:warp.dev:resource:docs-llms'); + assert.equal(entry.url, `${DOCS_ORIGIN}/llms.txt`); + assert.equal(entry.type, 'text/plain'); + const astroConfig = await readFile(repoFile('astro.config.mjs'), 'utf8'); + assert.match(astroConfig, /from 'starlight-llms-txt'/, 'starlight-llms-txt generates /llms.txt'); +}); + +test('discovery manifests do not use retired Oz wording or URLs', async () => { + const serialized = JSON.stringify([catalog, await readJson('public/.well-known/mcp.json')]); + assert.doesNotMatch(serialized, /\boz\b/i); + assert.doesNotMatch(serialized, /\/oz(?:[/#?"]|$)/i); +});