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: 11 additions & 1 deletion lib/mcp_registry/documents.ex
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,17 @@ defmodule McpRegistry.Documents do
|> String.downcase()

String.valid?(body) and not String.contains?(type, "html") and
not String.starts_with?(head, ["<!doctype", "<html", "<?xml", "<head"])
not String.starts_with?(head, ["<!doctype", "<html", "<?xml", "<head"]) and
not json?(type, body)
end

# The other catch-all: an API host answering every path with a JSON status
# document. Neither llms.txt nor AGENTS.md is JSON, and one that carries a
# timestamp would otherwise "change" on every check.
defp json?(type, body) do
String.contains?(type, "json") or
(String.starts_with?(String.trim_leading(body), ["{", "["]) and
match?({:ok, _}, Jason.decode(body)))
end

defp header(%Req.Response{} = resp, name) do
Expand Down
7 changes: 7 additions & 0 deletions test/mcp_registry/documents_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ defmodule McpRegistry.DocumentsTest do
assert [%{fields: %{"file" => ["absent", "published"]}}] = changes()
end

test "a JSON status page served at /llms.txt is not an llms.txt" do
# Seen in production: an API host answering every path with its health
# document, as text/plain.
serve(&text(&1, 200, ~s({"status":"ok","server":"mock-mcp-server"})))
assert Documents.check(doc()) == :missing
end

test "an outage is not a removal" do
d = doc()
serve(&text(&1, 200, "# Acme\n"))
Expand Down
Loading