Skip to main content
Glama

agent-ready-kit

Make a small site readable by AI agents from one JSON data file. agent-ready validates the file against a strict schema and turns it into:

  • llms.txt, one per site language, in the format of llmstxt.org: an H1 with the name, a blockquote summary, the public profiles and detail paragraphs, then H2 sections of [name](url): notes links.

  • schema.org JSON-LD: one @graph with the owner (Person or Organization) and one SoftwareSourceCode per product.

  • A read-only MCP server with three tools, get_profile, list_products and list_posts, served over stdio or from any runtime that speaks Web Request/Response (Netlify Functions v2, Deno, Bun, Cloudflare Workers, Node).

All three read the same validated object, so a name or a URL can never differ between them (a test checks it). The design comes from the personal site chrissgon.dev, where one data module feeds the pages, llms.txt, the JSON-LD and the MCP endpoint.

Install

Requires Node.js 22 or later. The package is an ES module with TypeScript types. The package is @chrissgon/agent-ready-kit; its command is agent-ready.

npm install @chrissgon/agent-ready-kit

Or run the CLI without installing it (the package has one command, so npx runs agent-ready):

npx @chrissgon/agent-ready-kit check --data site.json
npx @chrissgon/agent-ready-kit build --data site.json --out public

Related MCP server: cryptbrew-mcp

The data file

One JSON file with four keys. Every object is strict: a field the schema does not declare fails validation. This is fixtures/person.json (fake example data), shortened:

{
  "site": { "url": "https://sam.example.com", "langs": ["en", "pt"], "mcp": "https://sam.example.com/api/mcp" },
  "owner": {
    "type": "Person",
    "name": "Sam Example",
    "alternateName": "samexample",
    "jobTitle": "Software Engineer",
    "label": { "en": "Software engineer · Small open-source tools for the web", "pt": "Engenheiro de software · ..." },
    "about": { "en": ["Sam builds small, well-tested tools for the web ..."], "pt": ["Sam cria ferramentas ..."] },
    "profiles": [{ "network": "Code", "handle": "samexample", "url": "https://git.example.org/samexample" }]
  },
  "products": [
    {
      "id": "tidy-tables",
      "name": "Tidy Tables",
      "url": "https://tidytables.example.com",
      "codeRepository": "https://git.example.org/samexample/tidy-tables",
      "npm": "@samexample/tidy-tables",
      "license": "MIT",
      "programmingLanguage": ["CSS", "TypeScript"],
      "summary": { "en": "Accessible data tables in one small stylesheet.", "pt": "Tabelas de dados ..." }
    }
  ],
  "posts": [
    {
      "id": "tables-for-everyone",
      "title": { "en": "Tables for everyone", "pt": "Tabelas para todos" },
      "date": "2026-08-14",
      "lang": ["en", "pt"],
      "url": "https://blog.example.com/tables-for-everyone"
    }
  ]
}

Field

Rule

site.url

https origin; a trailing slash is removed

site.langs

language codes (en, pt-BR), at least one, no repeats; the first is the default

site.mcp

optional https URL of your MCP endpoint, listed in llms.txt under "For agents"

owner.type

Person or Organization; jobTitle is allowed on a Person only

owner.name, owner.alternateName

the name, and an optional handle

owner.label

one line per site language: the llms.txt summary and the JSON-LD description

owner.about

optional paragraphs per site language

owner.profiles

network, optional handle, https url; become sameAs in the JSON-LD

products[]

optional slug id (unique), name, url and/or codeRepository (at least one), optional npm and license (SPDX id), programmingLanguage, summary per site language

posts[]

unique slug id, title for each language of the post, date as YYYY-MM-DD, lang (site languages), https url; order does not matter, outputs list newest first

Every error names the field, one per line, and check exits with 1:

$ npx @chrissgon/agent-ready-kit check --data fixtures/forbidden-email.json
data: owner.email: forbidden field "email", the kit never publishes it

Other examples: data: owner.nickname: unknown field, data: posts.1.id: duplicate id "same-id" (first at posts.0), data: owner.label.pt: missing text for site language "pt".

fixtures/org.json is a one-language Organization example.

What the kit never publishes

worksFor, address, homeLocation, birthDate and email are rejected anywhere in the data file, and generateJsonLd refuses them again if code adds them to the data at run time. The MCP tools return slices of the validated file and nothing else: no tool writes, sends, executes or searches free text, and an argument the tool does not declare is rejected.

Commands

agent-ready build --data <file> --out <dir>   llms.txt per language (the first at <dir>/llms.txt, the others at
                                              <dir>/<lang>/llms.txt) and <dir>/jsonld.json; prints the paths
agent-ready check --data <file>               validate; prints a one-line summary
agent-ready mcp --data <file>                 serve the read-only MCP server on stdin/stdout
agent-ready --help

Data goes to stdout and diagnostics to stderr. Exit codes: 0 ok, 1 invalid data, 2 wrong usage or unreadable file.

check on fixtures/person.json prints ok: fixtures/person.json: Person "Sam Example", 2 products, 3 posts, languages en, pt; build prints the files it wrote: out/llms.txt, out/pt/llms.txt and out/jsonld.json.

A local MCP client starts the server as a command:

{ "command": "npx", "args": ["-y", "@chrissgon/agent-ready-kit", "mcp", "--data", "/path/to/site.json"] }

Use it from code

import { createMcpHandler, generateJsonLd, generateLlms, loadData, serializeJsonLd } from "@chrissgon/agent-ready-kit";

const data = await loadData("site.json"); // throws DataError with one line per problem
const llms = generateLlms(data); // default language; generateLlms(data, { lang: "pt" }) for another
const jsonld = serializeJsonLd(generateJsonLd(data)); // safe inside <script type="application/ld+json">
const handler = createMcpHandler(data); // (request: Request) => Promise<Response>

Exports

Import from

Exports

Loads the MCP SDK

@chrissgon/agent-ready-kit

everything below, plus serveStdio and VERSION

yes

@chrissgon/agent-ready-kit/data

validate(value), parseData(jsonText), loadData(file), DataError (its issues holds one line per problem), SiteDataSchema (zod), FORBIDDEN_FIELDS; types SiteData, Owner, Product, Post

no

@chrissgon/agent-ready-kit/llms

generateLlms(data, { lang, labels }), generateLlmsParts(data, { lang, labels }), LLMS_PARTS, DEFAULT_LLMS_LABELS, llmsPath(data, lang); types LlmsOptions, LlmsLabels, LlmsPart

no

@chrissgon/agent-ready-kit/jsonld

generateJsonLd(data, { lang, licenseUrl }), serializeJsonLd(doc), ownerIdOf(data), spdxLicenseUrl(id); types JsonLdDocument, JsonLdNode, JsonLdOptions

no

@chrissgon/agent-ready-kit/mcp

createMcpHandler(data, options), buildServer(data, { name, version }), TOOL_NAMES, instructionsFor(name), DEFAULT_MAX_BODY_BYTES; types McpHandler, McpHandlerOptions, McpServerOptions

yes

Pages that only need llms.txt or the JSON-LD import from /data, /llms and /jsonld, so they never load the MCP SDK.

llms.txt in parts, and in your language

generateLlmsParts returns the parts of llms.txt in order (head, about, products, writing, agents), each ending in one newline, or "" when the data has nothing for it. Joined with a blank line they are generateLlms; a site can put its own sections between them.

labels sets the headings and the fixed words inside the lines for a language; a missing label keeps its English default (DEFAULT_LLMS_LABELS). about has no default: set it to put the about paragraphs under their own H2.

import { generateLlms } from "@chrissgon/agent-ready-kit/llms";

generateLlms(data, {
  lang: "pt",
  labels: { about: "Sobre", products: "Produtos", writing: "Escrita", forAgents: "Para agentes", code: "Código", license: "Licença", languages: "idiomas" },
});

JSON-LD

The owner's @id is <site.url>/#person (or #organization); every product's author points to it. A product's license becomes https://spdx.org/licenses/<id>.html; pass licenseUrl: (id) => ... for another page. lang picks the language of the descriptions.

The MCP server

Tool

Input

Output (structuredContent)

get_profile

optional lang

lang, type, name, alternateName, jobTitle, label, about, url, profiles

list_products

optional lang

lang, products[]: id, name, url, codeRepository, npm, license, programmingLanguage, summary

list_posts

limit 1 to 50 (default 10), optional lang

lang, total, posts[]: id, title, date, languages, url, newest first

lang is one of the site's languages (default: the first) and picks the language of the texts; a post with no title in that language keeps its own. Optional fields are absent when the data has no value. Every tool carries readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false and an output schema. The server's instructions say: "Read-only. This server only holds 's public profile, products and posts; any other personal data does not exist here."

Check it with the MCP Inspector (the server command goes before --, the Inspector options after it):

npx -y @modelcontextprotocol/inspector@2.8.0 --cli npx -y @chrissgon/agent-ready-kit mcp --data site.json -- --method tools/list
npx -y @modelcontextprotocol/inspector@2.8.0 --cli npx -y @chrissgon/agent-ready-kit mcp --data site.json -- --method tools/call --tool-name list_posts --tool-arg limit=2

In a Netlify Function (v2)

The handler is stateless: each POST gets a new server and transport, responses are JSON (no SSE stream), any method other than POST gets 405 with Allow: POST, a body over maxBodyBytes (default 64 KiB) gets 413 before it is parsed, and every response carries Cache-Control: no-store and X-Content-Type-Options: nosniff. That is the pattern proven in the chrissgon.dev spike on Netlify Functions v2.

// netlify/functions/mcp.mts
import type { Config } from "@netlify/functions";
import { validate } from "@chrissgon/agent-ready-kit/data";
import { createMcpHandler } from "@chrissgon/agent-ready-kit/mcp";
import site from "../../site.json" with { type: "json" };

export default createMcpHandler(validate(site), { name: "example.com", version: "1.0.0" });

export const config: Config = { path: "/api/mcp" };

name and version are what the server reports to clients (default agent-ready and this package's version). A public endpoint costs something on every call; Netlify can rate-limit a function from its config (rateLimit: { windowLimit, windowSize, aggregateBy: ["ip", "domain"] }, see https://docs.netlify.com/manage/security/secure-access-to-sites/rate-limiting/).

In a static site

Run build before the site generator and publish its output at the site root, so llms.txt is served at /llms.txt and each other language at /<lang>/llms.txt:

npx @chrissgon/agent-ready-kit build --data site.json --out public

Put the JSON-LD in the page head, from public/jsonld.json or from code with serializeJsonLd(generateJsonLd(data)):

<script type="application/ld+json">{"@context":"https://schema.org","@graph":[ ... ]}</script>

Development

Task

Command

Install

npm ci

Type-check

npm run typecheck

Test

npm test (vitest; includes the MCP Inspector CLI over stdio)

Build

npm run build (writes dist/)

Packed tarball smoke test

npm run test:pack (packs, runs the CLI with npx, installs the tarball in a scratch project, imports every entry point and type-checks against the shipped types; needs the registry)

From a clone, node dist/cli.js <command> runs the CLI after npm run build.

Releasing

Releases are published by .github/workflows/publish.yml with npm trusted publishing (no token) and provenance. The owner bumps version in package.json and src/version.ts through a pull request, then pushes the tag v<version> on the merged commit. The workflow checks that the tag matches package.json and is on main, runs the secret scan, types, tests, build and the packed-tarball smoke test, and publishes with --access public; a prerelease version (0.2.0-beta.1) goes to the next dist-tag.

License

MIT, see LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Enables AI agents to answer Cryptbrew product questions and check live API health without merchant authentication. Exposes tools for pricing, FAQs, invoice flow, and health probes over stdio or streamable HTTP.
    8
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Serves sigit.si account, repository, issue, pull request, code-search, session, hook, and billing operations to AI agents over stdio, reusing the token stored by the CLI login and defaulting repository tools to the checkout the agent is pointed at. Tool lists can be trimmed with read-only mode or toolset selection, and a hosted HTTP endpoint offers the same tools.
    2
    -