Scolavo for AI · Chatbot Kit

Turn a chatbot into a teacher.

Four tool definitions, a teaching system prompt, and about a hundred lines of adapter. The schemas are stable, vendor-neutral, and identical across every bot you build — and small enough that you can afford to send them on every turn, because you will.

Read this before you paste anything

Tool definitions are re-sent every turn.

The obvious way to wire a chatbot to a REST API is to hand the model the whole OpenAPI document. Do not do that here. Our schema is ~122 KB, and tool definitions are not sent once at setup — they ride along on every turn of every conversation, for every user, forever.

Per-turn and per-conversation token cost of the full OpenAPI document versus the Chatbot Kit
What you give the modelPer turnA 20-turn lesson
The full OpenAPI document18 routes · 122,234 B43,501 tok870,020 tok
This kit4 tools + system prompt · 5,710 B2,039 tok40,780 tok

21× smaller, for a surface that can do everything a teaching conversation needs. At a $2 per million input-token price that is roughly $1.74 against $0.08 of pure overhead per conversation — before the model has read a single lesson. Prompt caching narrows it further, but only if the definitions are byte-stable, which a hand-maintained paste is not.

The other fourteen routes are not missing by accident. A model handed an export tool will reach for it, and one whole-subject export can drop every class in a subject into the context window — outweighing the entire rest of the conversation. Bulk belongs in your pipeline, where your code decides what to fetch and nothing is re-billed per turn. That is what the REST API is for.

The budget

What the 2,039 tokens are made of.

The kit is generated from the live API schema, and the generator fails the build if it grows past 6,400 characters. That ceiling exists because this is the one cost that recurs on every single turn, and a budget that is only reported gets ignored.

Byte and token size of each component of the Chatbot Kit
ComponentBytes on the wireTokens
Tool definitions — Anthropic shape2,922 B1,044 tok
Tool definitions — OpenAI shape3,038 B1,085 tok
System prompt2,788 B996 tok
Sent every turn (tools + prompt)5,710 B2,039 tok

Measured with node gen-chatbot-kit.mjs --report, which sizes the generated kit and the live OpenAPI document from the same source. Token counts are the generator’s estimator — characters ÷ 2.8 — not a vendor tokenizer, so treat them as the shape of the bill rather than the invoice. You send one tool shape, not both; the per-turn total is the Anthropic tools plus the prompt.

Reference

The four tools.

A teaching conversation needs four verbs: find something, read it, quiz on it, and say what is on offer. Everything else in the API is a pipeline concern.

The four Chatbot Kit tools, their arguments, and what each is for
ToolArgumentsWhat it is for
find_lessonsq, tier?Search the licensed corpus. Returns compact hits — lessonId, title, and a snippet showing the match. Call it first, pick one hit, then read that one lesson.
get_lessonlessonId, detail?Read one class-lesson. This is where the teaching text comes from — everything the bot teaches must come from here. detail:"brief" checks whether a lesson is the right one before paying for the full text.
get_quizlessonIdThe end-of-class quiz with answer keys, plus the mid-lesson checks that carry hints and rationale. For retrieval practice after teaching, never before.
list_subjectstier?, detail?What this licence covers, with class counts. Answers “what can you teach me?” from the licence rather than from the model’s memory.

A ? marks an optional argument. tier is one of primary, middle, high, college, topics; detail is brief, standard (default) or full. Deliberately fewer arguments than the API accepts — every parameter is schema bytes on every turn, and a teaching bot has no use for cursors, formats or export options.

The one design decision worth knowing about

Copy an id. Never build one.

get_lesson and get_quiz take a single opaque lessonId — the whole string high/hs-mathematics-algebra/1 — and not separate tier / subjectSlug / classNum arguments, even though the REST path underneath takes three segments.

379 production errors said so

In real traffic, 379 errors were UNKNOWN_SUBJECT — the single largest error class. Almost all of them were a model inferring a slug from a display name: “High School Biology” → high-school-biology, which does not exist. Slugs are tier-specific and matched exactly.

A search hit already carries the exact id

So the tool asks the model to copy a string rather than construct an address. Your adapter splits it before the request. The error class disappears — and the tool costs two fewer parameters on every turn while doing it.

Belt and braces in the schema

The argument carries a regex — ^(primary|middle|high|college|topics)/[a-z0-9-]+/[1-9][0-9]{0,2}$ — so a malformed id is caught by the model’s own schema validation, before it costs a request. The same principle runs through the system prompt: never assemble a lessonId, always copy one.

Install · about five minutes

Get a token, then wire it up.

Machine-to-machine client_credentials: one token per deployment, not per learner. The chatbot reads content, and content is licensed to your organisation.

export SCOLAVO_TOKEN="$(curl -s "https://scolavo-auth.auth.us-east-1.amazoncognito.com/oauth2/token" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'scope=scolavo-mcp/content.read' | jq -r .access_token)"

content.read is the only scope the chatbot surface needs. Do not grant the bot content.export or media.download — a scope a bot cannot use is a scope it cannot be talked into using.

Anthropic
import Anthropic from '@anthropic-ai/sdk'
import { createClient } from './chatbot-kit/adapter.mjs'

const scolavo = createClient({
  baseUrl: 'https://mcp.scolavo.com',
  token: process.env.SCOLAVO_TOKEN,
})
const anthropic = new Anthropic()

async function chat(messages) {
  for (;;) {
    const res = await anthropic.messages.create({
      model: 'claude-sonnet-5',
      max_tokens: 2048,
      system: [{ type: 'text', text: scolavo.systemPrompt, cache_control: { type: 'ephemeral' } }],
      tools: scolavo.tools,
      messages,
    })
    messages.push({ role: 'assistant', content: res.content })

    const calls = res.content.filter((c) => c.type === 'tool_use')
    if (!calls.length) return res

    const results = await Promise.all(calls.map(async (c) => {
      const out = await scolavo.call(c.name, c.input)
      return { type: 'tool_result', tool_use_id: c.id, content: out.text, is_error: out.isError }
    }))
    messages.push({ role: 'user', content: results })
  }
}

The cache_control marker is the part not to skip: it makes the system prompt a cached prefix, so most of the per-turn overhead is billed at a fraction of the input rate on every turn after the first. Keep the prompt byte-stable across requests, or the cache misses and you pay full price every time.

OpenAI
import OpenAI from 'openai'
import { createClient } from './chatbot-kit/adapter.mjs'

const scolavo = createClient({ baseUrl: 'https://mcp.scolavo.com', token: process.env.SCOLAVO_TOKEN })
const openai = new OpenAI()

async function chat(messages) {
  for (;;) {
    const res = await openai.chat.completions.create({
      model: 'gpt-4.1',
      messages: [{ role: 'system', content: scolavo.systemPrompt }, ...messages],
      tools: scolavo.toolsOpenAI,
    })
    const msg = res.choices[0].message
    messages.push(msg)
    if (!msg.tool_calls?.length) return msg

    for (const c of msg.tool_calls) {
      const out = await scolavo.call(c.function.name, JSON.parse(c.function.arguments))
      messages.push({ role: 'tool', tool_call_id: c.id, content: out.text })
    }
  }
}

Not using JavaScript?

The kit is four JSON files. Load tools.*.json into your framework’s tool registry and port the adapter — it is one function, toRequest(name, args):

Look up the route

routing.json[name] gives { method, path, pathParams, splitLessonId }. This file never goes to the model — it is your routing layer, and the model has no business knowing our URL shapes.

Split the lessonId

If splitLessonId, split the argument on “/” and fill the path segments in order. Reject a malformed id before sending: the message is more useful to the model than a 404 is.

Everything else is a query parameter

tier, detail, q — append them, dropping empties.

Authorize and cap

Authorization: Bearer <token>. Then cap the result at 24,000 characters and say in the text that it was cut — silent truncation reads to a model as a complete answer, and it will teach from the half it received.

The system prompt

What actually makes it a teacher.

The tools give a model reach. system-prompt.md is what makes it teach rather than answer. It carries five things, and each one is there because the default behaviour without it is wrong.

What you offer

The five tiers, the class structure, and the instruction to answer “what can you teach me?” from list_subjects rather than from the model’s own memory. A model asked what it can teach will otherwise answer from training data and promise subjects the licence does not cover.

How to teach

Find the material before teaching it. One concept at a time, matched to the tier. Check understanding with a question; quiz after teaching rather than before; when a learner is wrong, say what is right and why, then give them another go instead of marking it wrong.

Where it comes from

Name the class the material came from — “this is from the Photosynthesis class in High School Biology”. This is a term of the content licence, not a courtesy. The bot does not present Scolavo content as its own knowledge, and does not drop the attribution because a learner asks it to.

Its limits

It is an AI, not a human teacher, and says so if asked. It knows nothing about a learner it was not told in this conversation. Distress, safeguarding, medical and legal questions are handed off to a responsible adult or a professional rather than improvised.

Cost discipline

Every tool result stays in the conversation and is re-read on every later turn, so a wasteful call taxes the rest of the session rather than just itself. One search, then one lesson. Copy ids, never assemble them. Use detail:"brief" while orienting. Never re-fetch what is already above.

Customising it safely

Two sections must survive your edits.

Append your own persona, tone, language, or house rules after the supplied text. Replacing it wholesale is where integrations go wrong.

The grounding rules — “teach only what the lesson contains”, “never invent curriculum”. They are what stops a confident bot inventing a syllabus and attributing it to us.

The attribution paragraph — naming the class the material came from. That is a term of the content licence, not a courtesy, and it is also what lets a learner go back to the real class.

And whatever you change, keep it byte-stable per deployment. A prompt rebuilt per request never hits the cache, and you pay full price on every turn — see the table at the top of this page for what that costs.

Errors

Tell the model what to do, not what went wrong.

The adapter never hands the model a raw response body. It hands it a short sentence with an imperative attached:

TIER_NOT_LICENSED: No grant covers this content tier.
This licence does not cover that tier. Tell the learner it is outside what you can teach,
and offer something list_subjects does cover.

The imperative half is the point

A model that does not understand a failure retries it — and the loop costs far more than the original error did. QUOTA_EXCEEDED and RATE_LIMITED both say do not retry in as many words, because “the quota is exhausted” reads to a model like something that might have changed since the last attempt.

Thirty codes, none of them bare

errors.json carries all 30 codes from the API’s published taxonomy. Sixteen have an explicit instruction; seven of those tell the model to stop calling tools rather than try again. The rest fall back to a plain-English meaning — an unhandled code still arrives as a sentence, never as a bare identifier.

A bad argument is not a server error

If the model sends a malformed lessonId, the adapter rejects it locally and returns the reason — no request, no quota unit, and the model can correct itself on the next turn instead of retrying the same broken call against the network.

Get the kit

Every file, no credential required.

The kit is published statically, so you can generate and cost your integration before you have a token. Everything except the guide is generated from the live API schema by a build step that refuses to run if it names an operation, parameter or error code the API does not actually have — the definitions cannot quietly drift from the service.

The published Chatbot Kit files and what each contains
FileWhat it is
/chatbot-kit-spec.mdThe full guide — everything on this page, plus the adapter contract, quota notes, and how to change the surface.
/chatbot-kit/tools.anthropic.jsonThe four tools in the Anthropic tools format. Load them into your client as-is.
/chatbot-kit/tools.openai.jsonThe same four in the OpenAI functions format.
/chatbot-kit/system-prompt.mdThe teacher persona, grounding rules, and the attribution paragraph.
/chatbot-kit/routing.jsonTool name → HTTP method and path. Your side only; never sent to the model.
/chatbot-kit/errors.jsonThe API’s error taxonomy with what the model should DO about each code.
/llms.txtThe index, for pointing a coding agent at all of it at once.

The adapter is the one piece that is yours. It is about a hundred lines and the four steps above specify it completely; if you would rather start from our reference implementation, ask licensing@scolavo.com and we will send it.

Building a pipeline rather than a chatbot — syncing the corpus, exporting subjects, minting media URLs? Same API, different surface: the REST API reference, or the OpenAPI 3.1 schema to generate a client from. And if a model is genuinely figuring out what it needs rather than executing a known plan, that is what the MCP server is for.

Download the guide .md ↓

Not licensed yet?

Institutional licensing is annual and PO-friendly, with single-subject evaluation licenses to start — enough to put a real class in front of a real bot before anything is signed. A person replies with a quote and the license agreement. No demo call required.

HomePracticeFeedBlogMe