Repository navigation
Releases: basphere/ratatosk-mcp
Release list
v0.10.0
Changed
- The repository moved to the
basphereorganization, and every path that
carries the owner name moved with it: the Go module is
github.com/basphere/ratatosk-mcp, the image is
ghcr.io/basphere/ratatosk-mcp(also the chart'simage.repository
default), and the official MCP Registry name is
io.github.basphere/ratatosk-mcp. GitHub redirects the old web and git
URLs, but none of those three redirect:go geton the old module path
stays on the last tag published under it, the old image tags stay where they
are, and the old registry entry becomes a separate server that no longer
receives updates. Existing installs keep running — they reference tags that
still exist — but upgrading means pointing at the new paths.
v0.9.1
Fixed
check_stackrefuses a component that carries no project instead of
scanning every project for it. A model that sentproject_slugwhere the
schema saysprojecthad the key dropped, and the empty name went upstream
as "no project filter": one call paged all 2,435 changes and returned
676,771 characters, which the calling model then timed out re-reading. The
refusal names the keys that arrived and shows the expected shape, so one
retry fixes it; the same call now gets a 318-byte answer and makes no
upstream request.maxComponentsbounded how many components fan out; this
bounds each one. Reported by the basphere hub from a live run.
Changed
- A
check_stackbriefing shortens quotes to 120 characters with an ellipsis;
detail:"full"andget_releasestill carry them verbatim. Measured over a
five-component stack, quotes were 36% of the entry payload (median 90
characters, longest 548), so this bounds the tail without reducing the
typical entry to a fragment. The briefing is where a caller decides what to
look at; the verbatim quote is one call away, where the decision is made.
v0.9.0
Changed
- The agent prompt now says what a tool answer costs, not just whether it is
useful. A measured run pulled 1.4M characters from one cluster-wide pod
listing injson— larger than the model's whole context window — because
the rules called that call useless without calling it expensive, and a
self-hosted model re-processes every past tool answer on every later turn.
The prompt now asks for the default (wide) listing, reserves full specs
for a named resource, startscheck_stackatbrief, and passes an explicit
limittolist_changes. Chart and kagent example only — no image change,
so ahelm upgradefrom a checkout picks it up without a release. check_stackbriefings carry a shorter tail:other_changesis capped at 25
per component instead of 100, with the overflow still counted in
other_changes_omitted.action_requiredandcheck_configare untouched
and remain uncapped. A measured five-component briefing was 85,948 characters
and this tail was most of it, for changes the server had already judged to
need no decision. Asking callers to narrow withseverity_mindid not work:
it went unused in 13 of 13 measured calls across two model fleets, so the
default had to be the budget.list_changesreturns 20 changes when nolimitis given, down from the
upstream default of 50. One measured default call was 104,463 characters.
Paging is unchanged — a caller syncing a local copy raiseslimitand follows
next_sinceas before.check_stackaccepts the argument shapes models actually send:components
JSON-encoded as a string, andnameas an alias forproject. Both were
refused before, and a refusal is not cheap on the other side — a self-hosted
model spent about two minutes per rejection regenerating arguments, 8 out of 8
calls in one measured session. Rejections that remain now carry a minimal
correct example instead of only echoing what arrived.
v0.8.0
Hosted usage now counts — one bit upstream, no record of anyone
The hosted endpoint reaches its upstream over an internal network, so upstream metrics could not tell a hosted user's call from the operator's own traffic — hosted adoption always read as zero. This release closes that blind spot.
- In HTTP mode, each caller is classified as
selforexternalagainstMCP_SELF_CALLERS(comma-separated addresses; an entry ending in.matches as a prefix), and only that class travels upstream asX-Ratatosk-Caller. - The address itself is compared in process and discarded — neither logged nor sent. What leaves the process is the one-bit class, nothing else.
- Unset — the default, and every self-hosted install — classifies all callers as external. It costs nothing and changes no behavior; upgrading is a version bump. No tool contract changes.
Full Changelog: v0.7.5...v0.8.0
v0.7.5
Hardening pass over the HTTP surface
A security review of the server turned up four ways an install could be worn down over time — none of them a data exposure, all of them about staying up. This release closes them. No tool contract changes; upgrading is a version bump.
- The HTTP server now sets its own timeouts. Go's zero-value server leaves read, write and idle deadlines unset, so a client that dribbles a request header one byte at a time could hold a connection open indefinitely — and do it before the rate limiter ever counted the request.
check_stackaccepts at most 100 components. A larger list fanned out to full upstream paging per component, so one call could turn into thousands of upstream requests and burn the caller's own rate budget.- Idle sessions expire after 30 minutes in stateful HTTP mode. Without a timeout the SDK keeps every session forever, so a long-running install that reconnects often grows in memory. Stateless mode — required by the 2026-07-28 MCP revision, and what the hosted endpoint runs — allocates no session and was never affected.
- Upstream failures no longer echo the request URL back to the caller. A failed call now answers with the class of failure (
upstream request failed (connection_refused)); the address stays in the server's own log.
Full Changelog: v0.7.4...v0.7.5
v0.7.4
A version whose line does not exist is a dropped prefix, not an empty answer
0.7.3 stopped comparing across release lines. This is the other half of that change.
Agents do not get versions handed to them — they read them off image tags, and an image tag rarely carries the release-line prefix. knative publishes knative-v1.12.0 while its images say v1.12.0. Pass the bare version and every one of that project's releases lands on the far side of the line gate, so the briefing comes back empty and reads as "nothing to do".
This is not confined to projects that publish several lines. A project with exactly one line has the same problem when that line is a prefixed one. Across the tracked corpus, five projects publish no main-line release at all: cloudevents, flatcar, knative, linkerd and openfeature.
check_stacknow says so. When the running version's line does not exist for that project,notenames the lines that do and includes a real tag from one of them, so the shape to send is shown rather than described.- It does not cry wolf. Projects whose main line genuinely exists — containerd, wasmcloud — produce no warning even though they also publish a second line. Verified against the live API in both directions.
If you pass versions read from image tags, this is the release that tells you when that lost something.
Full Changelog: v0.7.3...v0.7.4
v0.7.3
A repository can publish more than one thing, and versions across them have no order
containerd tags its Go API module api/v1.11.1 beside the runtime's own v2.2.5. Flatcar ships lts-4081.3.9 and stable-4593.2.4 as parallel channels. OpenFeature publishes core/, flagd/ and flagd-proxy/ from one repository. Each is a separate release line, and asking which of two lines is "newer" is not a hard question — it is a meaningless one.
check_stack was asking it anyway. An operator on containerd v1.7.28 was offered api/v1.11.0 as part of their upgrade path: a Go library, presented as work to do on their container runtime. It parsed, so nothing complained.
- Only the line your running version belongs to is compared. Everything else is dropped before a key is even computed, and
notereports how many and why. - The line is read from the tag — the maintainer already wrote it there — rather than kept in a hand-maintained prefix list. That list was itself a bug found the same day:
stable-was on it andlts-was not, so Flatcar's entire LTS train parsed to nothing and vanished from every range query. - Pass the tag as published, prefix included:
flagd/v0.16.1,lts-4081.3.9,api/v1.11.1. A barev0.16.1is read as the main line and will not find the flagd releases. - Projects that prefix every tag alike are unaffected.
knative-v…,edge-…and the like resolve to a single line for the whole project, so a same-line comparison behaves exactly as before. Across the tracked corpus, five projects out of seventy-five have more than one line.
This is the counterpart to 0.7.1's branch awareness, and deliberately not an extension of it. A branch is an ordered position within one line, which is why "already fixed at or below the running version" means something there. Lines have no order at all, so they are excluded from candidacy rather than compared and filtered.
Measured against the live API: containerd v1.7.28 drops the two api/ releases and keeps its twenty; openfeature at flagd/v0.15.0 returns the nine flagd releases and excludes ten from the other two products — where before it returned nothing at all, because those tags did not parse.
Full Changelog: v0.7.2...v0.7.3
v0.7.2
The per-caller limit now actually engages behind a CDN
0.7.1 introduced a per-caller rate limit for shared endpoints and keyed its buckets on X-Forwarded-For first. That was wrong behind a CDN. With no trusted_proxies configured, the reverse proxy sees a Cloudflare edge as its peer and writes that address into the header — and edges rotate, so a single caller collected a fresh bucket every few requests and the limit never fired. Sixty-two consecutive calls through the hosted endpoint all returned 200 where the sixty-first should have been a 429.
CF-Connecting-IPis now read first, thenX-Forwarded-For,X-Real-IP, and the peer address. Cloudflare writes that header itself and it cannot be forged from outside it.- Whatever proxy sits in front must set one of these and strip whatever the client supplied. A limiter keyed on a caller-controlled value is not a limiter. The chart values and the install guide now say so in all three languages.
Nothing else changed. If you self-host with the limiter off — the default — 0.7.1 and 0.7.2 are identical for you.
Full Changelog: v0.7.1...v0.7.2
v0.7.1
A briefing that knows which branch you are on
An operator running containerd 2.2.5 was told to act on two CVEs their own branch had already closed — CVE-2026-46680 in v2.2.4, CVE-2026-47262 in v2.2.5 itself. Both were real fixes, both had been backported across every supported branch on the same day, and check_stack reads only releases newer than the running version, so it saw the v2.3.x occurrences and nothing else. It is the failure get_matter exists to prevent, pointing the other way: not crediting an install with fixes it never received, but demanding work it finished a release ago.
- A matter already fixed at or below the running version, on the running branch, is no longer reported. How many were excluded, and why, goes into
note— nothing disappears without saying so. - Branch equality is what makes that safe. A backport to v2.1.9 proves nothing about v2.2.4, which was cut from a different branch and may never have received it. Only the running version's own branch is evidence, so that is the only thing the rule accepts.
same_matter_also_addressed_innow names every release on record that carried the matter, not only those inside the returned window. The window starts above the running version, which meant the branch an operator most needed to see was the one systematically missing from it.
Measured against the live API: 2.2.5 now returns no action_required and a note explaining the two exclusions; 2.2.3 correctly reports v2.2.4, its own branch's fix, with the four sibling branches listed; 2.1.8 reports v2.1.9 and excludes three.
Rate limits that hold up when several people share an endpoint
The hosted endpoint reaches the upstream API over an internal network, deliberately bypassing the CDN and reverse proxy so that /v1 paths — which carry project slugs and running versions — never reach an access log. The cost of that design was steeper than intended: with no forwarding header surviving the hop, the upstream could not tell hosted callers apart, so every hosted user in the world shared one bucket of 60 requests a minute. A single fifteen-component check_stack ate a quarter of it, and what a caller saw when the bucket ran dry was not an error but a briefing with fetch failed quietly filling in for components.
MCP_RATE_LIMIT_PER_MIN(chart:rateLimitPerMin), off by default. Each caller address gets its own budget of N tool calls a minute; over it,429withRetry-After.- This lives in the server rather than upstream on purpose. Forwarding the caller address to the API would have fixed the fairness and broken the separation that makes the hosted endpoint what it is — the web tier currently cannot correlate who asked with what was asked, because it never receives the who. A structural guarantee survives a future careless log line; a policy one does not. The address is a bucket key and nothing else: never logged, never sent upstream, dropped when its window expires.
- Off is the default because the same binary is what people self-host, and a self-hosted server is single-tenant.
- The upstream per-IP limit is now 1200 requests/minute (was 60). One tool call is not one request, and the documentation now states the two limits in their own units instead of implying they are the same number.
Also
- The install rehearsal extracted the tool list from
main.gowith a pattern that assumed one space afterName:. gofmt aligns struct fields, so the pattern matched nothing and the script exited before printing — an install failure in the logs, a stale regex in fact. It now tolerates alignment and fails loudly on an empty list.
Full Changelog: v0.7.0...v0.7.1
v0.7.0
Release notes, read as changes rather than facts
Ratatosk split its analysis pipeline: what can be decided mechanically is now done mechanically, and the model is asked only for what actually needs judgment. The unit that comes out the other side is no longer a fact carrying a severity — it is a change, described by three axes that answer three different questions. This release moves the whole server, chart, examples and documentation onto that model.
Upgrading: two tools were renamed. facts_by_entity is now changes_by_entity, and list_facts is now list_changes. A client naming the old ones will not find them, and any allow-list that enumerates tools — the kagent toolNames field, for one — needs the new names plus get_matter.
- Three axes on every change.
family(security/breaking/deprecated) says what kind of thing it is.bucket(action/check/plan) says how to act on it now.applies_ifsays whether it is yours to act on at all, structured into named targets wherever the server could parse it — so an agent greps the running configuration for["CreateContainer", "sandbox"]instead of interpreting an English sentence. get_matteris new. One matter across every release branch that addressed it, oldest first. This exists because the branches do not carry the same fixes: one containerd roll-up landed on five branches carrying ten, ten, four, four and two advisory ids respectively. An operator on the two-advisory branch, told only about the newest entry, would credit their install with eight fixes it never received.check_stacksays what it actually does. The briefing has always split onbucket, but its ownhint— the text an agent reads to decide what to do — still described the split as "critical/high". It now states the rule it follows, and says plainly thatseverityin a briefing is derived by this server from the entry's advisories: useful for ranking urgency, never for deciding who is affected.- The kagent agent was configured for tools that no longer exist. Its
toolNamesstill listedfacts_by_entityandlist_facts, which meant three tools that do exist were never exposed to it. Its system prompt also instructed the model about retired response fields (fixed_in,coverage,mandatory). Both are corrected in the chart template and the example manifest; the triage procedure itself — the measured rules about reclassification, unresolved conditions and roster coverage — is unchanged. - Documentation rewritten from live responses. The tools reference (en/ko/ja) now covers all seven tools with responses captured from the running API, including the parts that are easy to misread:
changes: []is an audited silence that ships the original note with it,max_severity: nullmeans "no advisory ids" rather than "nothing serious", andseqis an analysis cursor, not a timeline.
Full Changelog: v0.6.2...v0.7.0