Repository navigation
Suprnova v4.4.0
Added
-
Handlers become agent tools with
#[tool]. A routed#[handler]marked
#[tool]is a tool agents can call.#[tool_input]derives the input's
JSON Schema (draft 2020-12) from the struct and itsserdeandvalidator
attributes, so an agent is told what the server enforces.tool_scopes!
declares scopes, and aToolCallerargument tells the handler and its
policies who is calling: apublicguest, auseror anagentaccount.
A tool call runs the route's middleware, validation, policies and rate
limits, under per-caller and per-address budgets. An invalid or duplicate
name, a missing description or an undeclared scope fails the build. See
Agents. -
An MCP endpoint serves the application's tools.
Router::agents(AgentsConfig::new())servesPOST /mcpover MCP's
Streamable HTTP transport, revision 2026-07-28, and toinitialize-based
clients of revisions 2025-06-18 and 2025-11-25, without sessions. It serves
protected resource and authorization server metadata at the well-known
locations under the application's root, lists every tool to a client
without a token and only the tools a token may call to one with a token,
runspublictools without a token, answers 401 and 403 with the scopes a
call needs, so a client's first call of a tool that needs a token sends it
to sign in, refuses a foreignOrigin, and bounds the body, the answer
and the run time. Structured results follow the session's revision: on
2026-07-28 a tool's output schema and result pass through as declared, and
on the two older revisions a non-object output schema is left out of
tools/listand a non-object result comes back as text alone. -
Users approve MCP clients through a built-in OAuth server.
AgentsConfig::oauth_server(OAuthServer::new())mounts the new Magnetar
authorization server plugin: OAuth 2.1 with the authorization code grant and
PKCES256, clients identified by a client ID metadata document or dynamic
registration, exact redirect URIs, tokens bound to the MCP endpoint and the
approved scopes, refresh tokens rotated on every use, revocation, and tokens
stored only as digests. Default approval and connected-agents pages ship,
andConnectedAgentslists and revokes a user's grants. Its issuer is
APP_URL. -
Tokens from an outside identity provider sign users in.
OutsideIssuer,
added withAgentsConfig::resolver, accepts the user tokens a provider you
name signs, checked against its published keys for issuer, audience, expiry
and scope. It finds the user bysub, by an id the issuer assigns
(id_claim, such as Microsoft Entra'soid) or by a claim the token marks
verified. -
Agents hold accounts of their own under auth.md.
AgentsConfig::agent_auth(AgentAuth::new())serves auth.md v0.6.0 from the
new Magnetar agent auth plugin: anonymous,service_authand ID-JAG
registration, the/auth.mddocument, a claim page with a six-digit code,
revocation by token and by Security Event Token, registration rate limits
and audit events. A claim dispatchesAgentClaimed, delivered at least once
with a stableevent_id. Theagents:key,agents:revoke-alland
agents:prunecommands print a signing key, revoke every agent credential
and run the maintenance pass. -
Live actions can be agent tools, in the browser and headless.
#[tool]
on a Live action offers it to agents, with fields marked#[agent_visible]
as the result. The Live runtime registers a mounted component's tools with
WebMCP. An action markedheadlessis also callable over MCP with no
browser, through a sealed state handle bound to the caller; the Gate
abilitylive:{component}.mountchecks the mount properties an agent sends,
and#[resolve(...)]fills a property from the caller instead. -
Pages offer server tools to agents in the browser.
Router::share_tools,with_toolson a route, a group, an Inertia response
or a Live document, andPageTools::addchoose the tools each page
registers with WebMCP. A page lists only the tools its viewer may call, a
page of public tools stays shared in RenderCache, and the page refreshes its
data after a write tool. WithWEBMCP_ORIGIN_TRIAL_TOKENset, every HTML
response carries theOrigin-Trialheader. -
The React, Vue and Svelte kits register page and form tools. New
projects callinstallPageTools(router)and shipusePageTool, for a tool
that lives while its component is mounted, anduseFormTool, which makes a
form a tool that fills and submits it through Inertia. A destructive form is
filled and left for the user to submit.suprnova generate-typeswrites
each form request's input schema tofrontend/src/types/tool-schemas.ts.
The kits'core.ts,suprnova-webmcpand the Live runtime read a page's
tools by the keys they know and ignore the rest, so a later release can add
a field without breaking a scaffolded application. A change an older reader
must not ignore raises the payload'sv. -
Rust front ends compiled to WebAssembly register WebMCP tools. The new
suprnova-webmcpcrate registers typed page tools, with the input schema
the server derives, for as long as their handles live, and calls the
application's server tools with the session and the CSRF token. Built for a
target without a browser it compiles and answers unsupported. -
Tests call tools as any caller.
TestClient::tooland
TestClient::live_toolcall a tool as a page does, andTestClient::mcp
calls the MCP endpoint as a guest, a user with chosen scopes or an agent,
claimed or not, without an OAuth or auth.md ceremony.Agents::fakemints
a token for a chosen caller, andTestResponse::page_toolsreads the tools
a page offers. -
Test requests can come from an address and carry any body.
TestClient::with_peer_addresssends requests as if from that TCP peer, so
Request::ip()and limits keyed by the client address see it.
TestRequest::bodysends a body of any content type as it is. -
Applications build their own consent pages on
AgentApprovalsand
AgentClaims.AgentApprovals::of(&request)reads the signed-in user's
pending authorization as aPendingApprovaland records the decision as an
ApprovalDecision.AgentClaims::of(&request)reads aPendingClaimand
confirms it with the user's code as aClaimConfirmation. Both answer 403
to a tool call, so an agent never approves or claims for the user it acts
as. The framework's default pages use them too.ConsentPageHeaders
refuses framing withframe-ancestors 'none', and setsno-storeand
no-referrer, on every answer of a consent route, error pages included,
and keeps the application's nonce policy. -
url::same_origin_pathchecks a return address, and
Redirect::intended_urlreads the intended one.same_origin_path
accepts only a path that starts with a single/and holds no control
byte, so a sign-in page checks areturn_towith it before
Redirect::set_intended_urlstores it.Redirect::intended_url()returns
the addressRedirect::intendedwould follow, without spending it, as
Laravel'sgetIntendedUrldoes. -
An application's migrator can create the agents tables and a
case-insensitive email index.suprnova::agents::migrations()creates
both plugins' tables, only what is missing. Its rollback leaves them in
place, and its documentation gives the statements that drop them.
suprnova::auth::migrations::UniqueEmailIgnoringCase::on(table, column)
adds a unique index onLOWER(column)on SQLite, PostgreSQL and MySQL
8.0.13 or later. On MariaDB and older MySQL it checks that the column has a
unique index of its own and a_cicollation. It refuses a table that
already holds addresses that differ only in case, and its index name fits
every engine's identifier limit. -
Tests build a router with agent auth without a signing key.
suprnova::testing::install_test_agent_auth_key()gives the test process a
generated key when the environment names none. -
#[tool_output]and#[tool_schema]derive schemas through the
framework.#[tool_output]marks a tool's output type, any struct or
enum, and derivesSerializeandJsonSchemawith noserdeor
schemarsdependency of the application's own.#[tool_schema]gives a
type nested in a tool's input or output its schema, and
suprnova_webmcp::tool_schemadoes the same for a page tool's input. -
Tools can carry WebMCP's
debugginghint.
#[tool(annotations(debugging))]on a server tool or a Live action, the
debuggingoption of the kits'usePageToolanduseFormTool, and
PageTool::debuggingmark a tool meant for debugging and developer
tooling rather than end users. The MCP endpoint does not send it. -
New projects serve agents.
suprnova newwrites a fresh agent auth
signing key into.env, readable by its owner only (mode 0600), and
.env.examplenames both variables with no value. Every kit serves the
MCP endpoint, the OAuth server and agent auth fromsrc/agents.rs, and its
migrator creates both plugins' tables. The Inertia and Live kits' migrator
also creates a case-insensitive unique email index, and their sign-in sends
a signed-in user who opens a sign-in link on to a same-originreturn_to
and keeps the intended address through sign-in and registration. Every kit
folds an email address to ASCII lowercase before validating it, and bounds
names at 255 characters and addresses at 254. -
The agents maintenance pass prunes the OAuth server's ended rows. The
five-minute pass inserveandagents:prunedelete lapsed pending
authorizations and codes, expired tokens, and grants revoked longer ago
than the retention, whenever the router mounts the OAuth server, with agent
auth or without. A live grant or token is never pruned.agents:prune
needs agent auth only for--retry-failed. -
The Inertia and Live kits ship their own approval, claim and
connected-agents pages. The React, Vue and Svelte kits render
Agents/Approve,Agents/ClaimandAgents/Connectedat
/oauth/approve,/agent/claimand/connected-agentson the consent
facade, and an approval leaves for the client by a location visit. The
Live kit serves the same three pages as plain server-rendered documents
with native forms, and an approval leaves through a short page whose meta
refresh and link go to the client, whichform-action 'self'allows. Every
route runsConsentPageHeaders, no consent page offers a tool, and text a
client, an agent or the user chose sits in<bdi>. See
Starter Kits. -
The API kit serves agents, with a sign-in for consent only. A new API
project serves the MCP endpoint and both plugins with the framework's
default pages. A browser signs in at/loginonly to approve a client or
claim an agent: the session runs on the consent pages alone, ends after
15 minutes idle, keeps its cookie for the browser session, and ends at
POST /logout.GET /api/connected-agentslists the user's connected
clients and agents, andDELETE /api/connected-agents/clients/{grant}and
/agents/{registration}revoke one. Registration mails a link that
verifies the address, which a claim needs, and
POST /api/email/verification-notificationmails a new one. -
Auth::email_verification()verifies an address for its signed-in
owner.issuemints a single-use link token for the signed-in user, and
issue_forone for an account the request just registered. The
application mails the link.verifyverifies the address only for the
user signed in where the link opens, only while the account still holds
the address the link was mailed to, and answersVerified,
AlreadyVerified,OtherAccount,AddressChanged,Expired,Unknownor
SignIn. Anyone can register another person's address with a password of
their own, so a link that verified whoever opened it would verify that
account. Verifying changes no credential and starts no session. -
Consent pages refuse a form tool's submission. A kit's form tool sends
the form it submits with theSuprnova-Form-Toolheader, which
suprnova::agents::FORM_TOOL_HEADERnames.AgentApprovals::ofand
AgentClaims::ofrefuse it with 403, as they refuse a tool call, so a form
tool on an approval or claim page cannot approve or claim. -
New projects ship two notes tools. The Inertia and Live kits serve
search_notesatGET /notes/searchandcreate_noteat
POST /notes/tools, under thenotes:readandnotes:writescopes, each
acting on the caller's own notes. The notes page offers both to an agent in
the browser. -
The Live input takes
maxlength. The component library's input sets
the attribute only when a caller passes one. The Live kit's account forms
bound the name at 255 characters and the address at 254. -
#[tool]warns over WebMCP's budgets. The build warns when a tool's
name passes 30 characters, when its description passes 500, and when an
input field has no description, and boot logs the same. The warnings never
refuse a tool. A tool that keeps a long name on purpose sets
budget = ignorein its marking, which quiets the build warning for that
tool alone; boot still logs its budget warnings. -
Live tools describe their arguments.
arguments(amount = "How much to add.")on a Live tool marking gives each action parameter the description
an agent reads; an argument name the action does not have fails the build.
A mount parameter takes a doc comment. -
The Inertia frontends pin
webmcp-types. The React, Vue and Svelte
kits' browser helpers use the community'swebmcp-types0.1.10 definitions
for the model context, tool definitions and tool results.
Changed
- Tool names are identifiers. A tool name is 1 to 64 ASCII letters,
digits and underscores, so every model's tool calling accepts it; a name
with a dot or a hyphen fails the build. The shipped tools are renamed verb
first:posts.createiscreate_post,notes.searchissearch_notes.
A browser instance of a Live tool joins the tool name and the instance key
with a hyphen, and the whole fits in 64 characters: a root mount that does
not fit fails at declaration, and the Live checker reports an island that
does not. An application with dotted tool names renames them. MagnetarLifecycleEventandLifecycleEventcarry anattributes
map. Agent auth events record the registration, the client address and
the fields auth.md names for each kind in it; it is empty for the other
kinds. Code that builds either struct field by field, or destructures it
without.., no longer compiles.- Three public enums have new variants.
LifecycleEventKindgains the
nine agent auth audit kinds, fromAgentRegistrationCreatedto
AgentRegistrationRevoked.CryptPurposegainsLiveAgentHandleand
HandlerArgKindgainsContext. Amatchover one of them without a
wildcard arm no longer compiles. - The
agentsfeature is on by default. An application that keeps the
framework's default features compiles the agent tools, the OAuth server and
agent auth, and Magnetar'smigratecreates their eight tables when it next
runs. Nothing is served until the router callsRouter::agents. generate-typesandservewritefrontend/src/types/tool-schemas.ts.
The file holds the input schema of each form request, or an empty list,
beside the Inertia props types. Each run brings it up to date and names
each request it cannot describe exactly as the server does.Redirect::intendedfollows only a path, anhttporhttpsURL, or a
relative reference. It ignores a stored value with another scheme, such
asjavascript:,mailto:orlocalhost:3000/x, and one with surrounding
whitespace or a backslash anywhere, and redirects to its fallback instead.
A browser trims whitespace and reads\as/, so such a value could
leave the application. See
Storing the intended URL.- An Inertia error page keeps a
no-storethe replaced response carried.
The error page setCache-Control: no-cache, privateon every page it
rendered. It now setsno-storewhen the response it replaces said
no-store, so a page that must not be stored stays unstored when it fails. - Magnetar's
auth_tokenstable gains anaddress_digestcolumn. A
token that verifies an email address keeps the SHA-256 of the address it
was mailed to. Magnetar'smigrateadds the nullable column the first time
it runs. - A custom Magnetar engine, store or token binding refuses email
verification until it implements the new methods.
MagnetarPasswordAuthEnginegainsemail_verification_issueand
email_verification_verify,TokenStoregainsissue_boundand
inspect, andUserStoregainsmark_email_verified_for.TokenFields
gainsread_address_digestandwrite_address_digest. Each has a default,
so existing code compiles, and the defaults refuse, so
Auth::email_verification()fails closed until they are implemented.
Fixed
- Concurrent writes no longer fail on MariaDB 11.8 and later. These
releases turninnodb_snapshot_isolationon by default, under which a
transaction that updates a row another transaction changed after its
first read fails with error 1020, "Record has changed since last read",
where MySQL waits and works on the latest row. Two overlapping writes to
one table failed one of them with a 500, because each advances the
RenderCache ledger, and two requests spending one email verification or
password reset link at once could fail the losing request with a database
error instead of answering that the link was used. Every connection the
framework opens to MySQL or MariaDB now runs
SET SESSION innodb_snapshot_isolation = OFFonce, when it opens. A
server without the variable, MySQL included, refuses it as unknown and
the connection opens as before. The statement never fails a connection:
any other refusal, as a MySQL-compatible proxy or service may give, is
logged once per pool atwarn, and the connection opens with the
server's own setting. See
MySQL semantics on MariaDB. DB::transaction_with_attemptsretries by the database's error code.
It re-runs an attempt the database refused with MySQL or MariaDB error
1213, 1205 or 1020, or Postgres SQLSTATE 40001 or 40P01, and returns any
other database error at once, even one whose message mentions a
deadlock. It used to match the word "deadlock" in the message, so a lock
wait timeout and MariaDB's error 1020 were never retried. An error with no
database code is still matched by its message.- A Postgres schema dump loads where an administrator installed an
extension.schema:dumpcopied the comment an extension sets on
itself, such asCOMMENT ON EXTENSION vector. Loading that dump as the
application's role failed with "must be owner of extension", because the
extension belongs to the role that installed it. The dump now leaves those
comments out, andCREATE EXTENSIONsets them again. - Magnetar's tables hold long values on MySQL and MariaDB. Byte
columns were limited to 255 bytes. These hold passkey and OAuth sign-in
steps, two-factor secrets and recovery codes, and saved provider tokens.
So were the session user agent and the passkey credential id, public key
and provider scope columns. A longer value failed to save, so passkey,
two-factor and social sign-in could fail there. Magnetar'smigrate
widens the existing columns the first time it runs. It keeps each
column's character set and collation, and leaves a column that an index
covers alone, with a warning. - Two servers migrating Magnetar's tables at once no longer collide. A
table, index, column or marker that another process created first counts
as created. - Registration answers a validation failure with 422.
Auth::password().registeranswered a weak password or an unusable
address with 401. It now answers the framework's validation error, with
the message under the field. - Email addresses longer than 254 characters never reach the database.
Sign-in with one answers as an unknown address does. Sign-up, the account
import and OAuth email completion refuse one as a validation error. Such
an address is still counted toward lockouts. An account that already
holds one can no longer sign in by email;migratelogs how many exist,
once per database. - Session metadata stays within its columns. A session keeps at most
1,024 characters of the user agent. A lockout records the client address
only when it parses as an IP address. - New projects get every default feature of the framework. A new
project'sCargo.tomlturns the framework's default features off and lists
the ones it keeps. The list lackedsearch-mediaandagents, so a new
project had neither media search nor agent tools. It now lists both. - A form request on GET or HEAD reads its query. As Laravel validates the
query of a GET, a form request on a GET or HEAD request without a body
reads its query string as it reads a url-encoded body: it decodes,
validates and names its fields asrequest.query_into()does. Such a
request was refused with 415. A request with a body still reads the body,
and other methods are unchanged. - Precognition and the real request read a query alike. A query value
holding a JSON object reads as that object where a struct or a map belongs,
and stays text everywhere else, in Precognition, a GET form request and
request.query_into(). Precognition decoded the object wherever it stood,
so a route could pass Precognition and fail the real submission. - An
OptionLive mount parameter can be left out. Every mount parameter
was registered as required, and no codec decodes null, so anOption<T>
parameter could never beNone. It is now optional: left out or null, it
mounts asNone. - A page's scripts and styles no longer become the previous URL. The
session recorded any successful GET as the previous URL: a script or
stylesheet thatStaticFilesserved, a request a global middleware
answered before any route matched, and an AJAX request. Without a
Referer,Redirect::back()could then send the user to a.jsfile. As
in Laravel, the session now records a GET only when a route answered it,
the fallback route included, and skips a request that sends
X-Requested-With: XMLHttpRequest. A file thatStaticFilesserves is
never recorded. - Two uses of one email verification link at once no longer answer 500 on
MariaDB. MariaDB 11.8 and later end a transaction that writes a row a
concurrent one changed, so the second of two concurrent uses of one
Magnetar verification token answered 500. The losing use now reads the
token as already used, on every engine. A consume the database ends in a
deadlock or a serialization conflict runs again, up to three times, and
answers a conflict rather than an internal error if every run conflicts. - Pages keep their styles under the Vite dev server. In development,
SecurityHeadersallowed the dev server inscript-srcandconnect-src
only, so the browser refused the stylesheet link the framework puts in the
head, and a page under the balanced preset rendered with no styles. The
dev server is now allowed instyle-src,font-srcandimg-srcas well. - A panic the panic hook reports carries its request. A panic raised
inside a request or job scope but outside the framework's own catch
reached the tracker without the scope's request id, request, route and
user, which an error reported in the same scope carries. It now carries
them. The hook never waits on a lock the panicking thread may still hold:
a field it cannot read at once is left out, and if the request's auth
state is locked, the event names no user rather than a wrong one. - A panic reported while another report resolves its stack is kept. The
panic hook gave up the whole event when the symbol resolver was busy, so
the one event a reporter exists for could vanish and only a counter said
so. The hook now sends the event without frames and says so in its
payload; the message, location, request and release all arrive.