Changelog¶
All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
[0.17.1] - 2026-08-23¶
Added¶
- Benchmarks page: measured throughput against other frameworks, and the method behind the numbers. (#286)
Changed¶
GZipMiddlewarecompresses a buffered body belowmin_stream_chunk_offloadinline instead of on the thread pool. Measured at 32 concurrent requests: 12,489 vs 5,393 requests per second for a 6 KiB body, 5,432 vs 4,102 at 32 KiB; past roughly 48 KiB the pool wins and is still used. (#286)
[0.17.0] - 2026-08-23¶
Added¶
VeloceDeprecationWarningcarries every Veloce deprecation and is visible under the default warning filter. (#284)url_foris importable from the top level, building a URL against the active app. (#284)UploadFile.save_asyncstreams an upload to disk without blocking the event loop. (#284)WebSocket.accepted_subprotocolreports the subprotocol the connection settled on. (#284)Capability,Transport,BidirectionalTransportandregister_sse_transportare exported fromveloce.contrib.mcp. (#284)MCPContextexposesclient_id,request_id,task_id,origin_request_id,transportandlifespan_context. (#284)
Fixed¶
TrustedHostMiddleware,HTTPSRedirectMiddlewareandCSRFMiddlewarestand down for a replayed MCP call, which they previously refused. (#284)add_middleware(instance, name="x")applies the name, soexclude_middleware=["x"]matches it. (#284)- A class-based view receives its path parameters;
MethodView.get(self, request, uid)no longer raises. (#284) add_url_ruleregisters the verbs aViewdeclares instead of defaulting toGET. (#284)HTTPSRedirectMiddlewareignores anX-Forwarded-Protohop thatProxyFixrefused. (#284)render_template_stringresolves filters, globals and tests registered on the app. (#284)TestClient.websocket_connectsends aHostheader, which RFC 6455 Sec. 4.1 requires. (#284)- A conditional 304 advertises the representation's length instead of
Content-Length: 0, including when a handler and a middleware both downgrade it. (#284) - An
after_requesthook is called by its own signature, so one taking only(response)works. (#284) - Task augmentation is refused on every method that cannot run in the background, not just two. (#284)
veloce checkandveloce routesload the dotenv file, and accept--env-file/--no-env-file. (#284)CORSMiddlewaremergesAccess-Control-Expose-Headersinstead of discarding another middleware's entries. (#284)BadResetTokenis raised on misuse; it also subclassesTypeError, which was raised before. (#284)Request.is_disconnected()reports a real disconnect on astream=Trueroute. (#284)MAX_CONCURRENT_CONNECTIONSandWRITE_BUFFER_HIGH_WATERare seeded indefault_config(). (#284)- A multipart body that ends inside a part is refused with 400 instead of returning 200 with the field missing. (#284)
- A multipart upload's spool file is closed once the request is done with it, instead of surviving until collection. (#284)
Request.url_for(..., _external=True)builds from the request's recovered scheme, host, port andscript_root. (#284)
Deprecated¶
Veloce.on_event()warns throughVeloceDeprecationWarning; use@app.on_startup/@app.on_shutdown. Removal in v1.0.0. (#284)FileResponse(path)on a running loop warns throughVeloceDeprecationWarning; useawait FileResponse.from_path(path). (#284)
Changed¶
- Deprecation warnings are raised as
VeloceDeprecationWarningrather thanDeprecationWarning, which the default filter hid. (#284) import veloceno longer imports the MCP, OpenAPI or Redis integrations; they resolve on first use. (#284)MAX_CONTENT_LENGTHis enforced once per request by the transport that read the body, instead of again during dispatch. (#284)add_middlewareraisesTypeErroron a construction argument passed with an already-built instance, instead of dropping it. (#284)- The templating error names
Veloce(template_folder=...)rather than a private attribute. (#284) url_for,url_path_forandRequest.url_fortake the endpoint positionally, so a route may have a{name}segment. (#284)CORSMiddlewaresendsAllow-CredentialsandExpose-Headersonly when an origin was allowed. (#284)SessionAuthlets a missingSessionMiddlewaresurface instead of masking it as an anonymous request. (#284)TestClientpercent-decodes the request path, as an ASGI server does. (#284)ToolSearchtools are counted by the MCP scoped-tool scan. (#284)- A class-based view is forwarded only the path parameters its target declares; reading
request.path_paramsstill works. (#284)
[0.16.0] - 2026-08-23¶
Security¶
- Replaying an MCP refresh token revokes the whole token family, per OAuth 2.1 Sec. 4.14.2. (#282)
- A trusted
Forwardedheader is the sole authority forfor,protoandhost; a hop refused by trust depth can no longer set them throughX-Forwarded-*. (#282) RateLimitMiddlewarekeys on the caller's address under ASGI; a changingUser-Agentno longer bypasses it. (#282)
Added¶
MCPContext.request_metareads the_metathe client sent with this request. (#282)add_mcp_proxy(scopes=..., tags=...)puts an upstream's tools behind a scope or a label. (#282)- The 26 named HTTP exception classes are exported from the top level. (#282)
VeloceErrorroots every exception Veloce raises; every existing base is kept. (#282)QueryParams,Cookies,StateandAddressare exported from the top level. (#282)- The ten signals are exported from the top level. (#282)
HealthPluginis exported from the top level. (#282)ServerNotImplementednames the 501 exception class;NotImplemented_still resolves to it. (#282)- A resource template accepts
{+name}, binding a whole path - separators included - to one variable. (#282) veloce mcp runserves an app's MCP tools, which is what a client config file launches. (#282)veloce mcp listprints the tools, resources and prompts a client would see. (#282)MCPAuthorizationServerissues MCP tokens: OAuth 2.1 with PKCE, refresh rotation, and RFC 7591 registration. (#282)register_authorization_servermounts its metadata,/authorize,/tokenand/register. (#282)AuthorizationStoreandInMemoryAuthorizationStoreback the issued clients, codes and tokens. (#282)mount_mcp(transport="sse")serves the deprecated split-endpoint SSE wire. (#282)mount_mcp(session_backend=...)shares HTTP MCP sessions between workers. (#282)SessionBackendandSessionRecordare exported for implementing that store. (#282)mount_mcp(page_size=...)paginates the MCP list methods with the spec's opaque cursor. (#282)- A tool may return a list of content blocks, emitted in order as the result's
content. (#282) MCPContext.statereaches the call's request state, so a handler holding the context can stash a value. (#282)mcp_tool(tags=...)labels a tool, and every tool exposestagsfor a visibility policy. (#282)- A
pathlib.Pathparameter declaresformat: pathin the tool schema. (#282) mcp_resource_mime_type=declares the media type a resource listing advertises. (#282)mount_mcp(tool_search=...)publishessearch_tools,describe_toolsandrun_toolsin place of the catalogue. (#282)run_toolsruns several declared calls in one request, passing results between steps. (#282)mcp_tool(version=...)registers several versions of a tool under one published name. (#282)MCPContext.sample_with_toolsruns the sampling loop, executing the tools the model asks for. (#282)SamplingRunandSampledToolCallreport a run's answer, transcript and tool calls. (#282)derive_toolandArgTransformpublish a narrower façade over a registered tool. (#282)app.add_mcp_toolregisters an already-built tool. (#282)MCPContext.hide/unhide/reset_visibilitynarrow one connection's view of the catalogue. (#282)app.mount(..., expose_mcp=True)publishes a sub-application's MCP primitives through its parent. (#282)add_mcp_proxyserves another MCP server's tools from this app, forwarding each call. (#282)@app.before_mcp_calland@app.after_mcp_callrun around every MCP call, route-backed or not. (#282)MCPContext.result_metaattaches_metato the result of the call being handled. (#282)meta=onmcp_tool/mcp_promptandmcp_meta=on a route publish_metaon the definition. (#282)mcp_resource_size=andmcp_resource_annotations=declare what a resource listing advertises. (#282)Veloce(website_url=..., mcp_icons=...)publishes them in the MCPserverInfo. (#282)MCPContext.sample(include_context=...)asks the client to attach server context to the prompt. (#282)mcp_tool(annotations=...)declares the behaviour hints a tool with no HTTP verb cannot derive. (#282)render_template,render_template_stringandstream_templateare exported fromveloce.contrib. (#283)MCPRequestErroris exported fromveloce.contrib.mcp.transports. (#283)mount_mcp(tool_filter=...)narrows which toolstools/listreports per caller. (#282)MCPContextreportssession_id,client_info,client_capabilitiesandis_background_task. (#282)MCPContext.client_supports(name)tests an advertised client capability, nested with dots. (#282)MCPContext.debug/info/warning/errorare shorthands for the matchingloglevel. (#282)MCPContext.read_resourceandget_promptreach the server's own components, scope checks included. (#282)MCPContext.list_resourcesandlist_promptsenumerate what the list methods report. (#282)MCPContext.send_notificationsends an arbitrary JSON-RPC notification to the client. (#282)- Cacheable MCP results carry
ttlMsandcacheScopeon the modern revision. (#282) mount_mcp(cache_ttl_ms=...)sets the freshness hint sent with those results. (#282)subscriptions/listenopens a filtered notification stream, replacingresources/subscribe. (#282)notify_tools_list_changed()andnotify_prompts_list_changed()signal those lists changed. (#282)- MCP tasks are served as the
io.modelcontextprotocol/tasksextension on the modern revision. (#282) tasks/updatedelivers responses to a task's outstanding input requests. (#282)
Changed¶
- The built-in server buffers a route's body before dispatch unless it declares
stream=True; declare the flag to keep incremental delivery. (#282) app.mount(..., expose_mcp=True)takes its flag as a keyword; passing it positionally is refused. (#282)- A stateful connection advertises
listChanged: truefor tools, prompts and resources. (#282) - A tool, prompt or resource whose declared
scopesthe caller lacks is no longer listed. (#282) - A resource list narrowed by declared scopes is marked private, so a shared proxy cannot reuse it. (#282)
resources/readandprompts/getrefuse a task-augmented request instead of answering synchronously. (#282)- A modern client must declare the tasks extension before a task handle is returned. (#282)
tasks/listandtasks/resultare not served to a modern client;tasks/getcarries the result. (#282)pingandlogging/setLevelare not served to a modern client; both revisions keep their own surface. (#282)- A modern client sets its log level per request via
_meta; a request naming none receives no log notifications. (#282) tools/list,prompts/listandresources/listbuild each entry once and reuse it. (#283)veloce.appexportsVeloce,URLRuleandPlugin;import *no longer pulls in stdlib names. (#283)- A test-client websocket read raises
RuntimeError, not bareException, when the peer closes. (#283)
Fixed¶
- A
typing_extensions.TypedDictis recognised as an object shape; it was previously advertised as a string. (#282) - A
TypedDictPydantic cannot adapt on this interpreter falls back to a plain mapping instead of failing the request. (#282) .js,.json,.css,.svgand.wasmare served with their standard media type regardless of the host registry. (#282)- The gunicorn worker warns when its TLS certificate is expired or not yet valid. (#282)
- A dropped SSE client no longer leaves the in-flight call buffering notifications nobody reads. (#282)
- Closing a stdio MCP connection reclaims the tasks it created, including one that never settles. (#282)
- The gunicorn worker honours
--ssl-versionas a minimum TLS version; a floor below the interpreter default is refused and logged. (#282) request.schemereportshttpson a TLS connection served by the built-in server or the gunicorn worker. (#282)X-Forwarded-Protono longer sets the scheme from a hopProxyFixrefused. (#282)- A task-augmented
tools/callis refused on a connection with no session, which previously pinned an unreachable task. (#282) MCPContext.session_idis unique across worker processes, so per-client state is no longer shared between unrelated clients. (#282)- Graceful shutdown closes idle keep-alive connections before awaiting the server, so shutdown hooks run instead of being killed. (#282)
request.get_json()andrequest.dataread the body under the built-in server and the gunicorn worker. (#282)- A response's ETag,
Last-Modified,ExpiresandVaryare read whatever casing wrote them. (#282) - An
If-Matcha response satisfies is no longer refused when its ETag was written asEtag. (#282) FileResponsenames its media type through the same memoized lookup the static server uses. (#282)StaticFileshonours anIf-RangeETag a subclass emitted with surrounding whitespace. (#282)- A proxied call forwards the caller's
_meta, so an upstream sees the progress token. (#282) run_toolsrefuses a plan whose step ids repeat instead of mis-resolving a$fromreference. (#282)- A
$frompointer of/names the member keyed"", as RFC 6901 defines it. (#282) - An array index in a
$frompointer is refused unless it is unsigned and unpadded. (#282) - A too-deeply-nested step argument fails that step rather than the whole plan. (#282)
- A proxied result whose content block is not an object no longer ends the plan. (#282)
SamplingRun.messagesends with the answer, so extending it for another run keeps it. (#282)- A sampled tool's
structuredContentreaches the model instead of only its text. (#282) - A fractional number is refused where a tool declares an integer, instead of losing its fraction. (#282)
- A number or a string is refused where a tool declares a boolean. (#282)
derive_toolrefuses to derive from a derived tool, which published a surface no call could satisfy. (#282)ArgTransform(schema=...)refuses a type the handler behind it would reject. (#282)- A copied tool no longer advertises versions only the tool it was copied from can serve. (#282)
- A proxied tool drops the upstream's version metadata, which the gateway cannot honour. (#282)
- A listing a connection can narrow is marked
private, so a shared cache cannot replay it. (#282) MCPContext.hidenarrows whatsearch_toolsanddescribe_toolsreport, not only the listing. (#282)- A server that can narrow nothing no longer rebuilds its catalogue on every discovery call. (#282)
- Every
MCPErrora tool handler raises reaches the caller with its code, message anddata. (#282) - A route-backed tool's
MCPErroris delivered instead of being rendered as an HTTP error body. (#282) MCPContext.hideannounces only the listing the hidden name belongs to. (#282)- A
list_changednotification is no longer sent for a capabilityinitializedid not advertise. (#282) - An MCP tool result encodes through the framework's own encoder, so both doors answer the same JSON. (#282)
- A
Secretin a tool result is refused, as it already was on the HTTP path, instead of being emitted. (#282) - A model's computed fields survive the
orjsonfallback, matchingjsonable_encoder. (#282) - A
msgspec.Structpublishes its fields instead of its Python repr. (#282) - The MCP protected-resource metadata advertises
bearer_methods_supported. (#282) - A
list[Model]tool parameter publishes the model as its item schema, not a string. (#282) - An MCP argument whose JSON type contradicts the published schema is refused, not passed to the handler. (#282)
- A JSON-RPC response POSTed to the HTTP transport is accepted with
202, not refused. (#282) - URL-mode elicitation sends the required
elicitationId, so a conforming client accepts it. (#282) - URL-mode elicitation is refused unless the client declared
elicitation.url. (#282) - A percent-encoded resource template value reaches the handler decoded. (#282)
- The most specific resource template serves a URI, not whichever was registered first. (#282)
- A path parameter no handler parameter declares is documented in OpenAPI and in the tool schema. (#282)
- JSON that is not a Request object is
-32600, not the-32700reserved for unreadable input. (#282) - A request carrying a null id is refused; MCP requires a string or integer id. (#282)
- An MCP call no longer leaves its request bound, which corrupted the HTTP transport's own request. (#282)
- An abandoned MCP request releases its cancellation-registry entry instead of stranding it. (#282)
- A tool returning a
@dataclassorTypedDictpublishes anoutputSchemaandstructuredContent. (#282) - An optional tool parameter advertises its null branch, and a parameter's default is published. (#282)
- A sub-dependency's body model advertises its fields, so a call built from the schema is accepted. (#282)
- A tool returning
bytesreports the decoded text, or base64 when the bytes are not text. (#282) - A
@dataclassparameter is validated and passed as the dataclass instead of failing on every call. (#282) - A
TypedDictparameter declares an object schema, matching what the handler accepts. (#282) client_host,client_portandremote_addrreport the peer on the ASGI path. (#282)- An authorization failure inside a tool is reported as forbidden, not as an internal error. (#282)
- A modern-revision client's identity and capabilities are read from each request's
_meta. (#282)
[0.15.0] - 2026-08-20¶
Added¶
- MCP serves the
2026-07-28revision alongside the handshake revisions, selected per request. (#276) server/discoveradvertises the served protocol versions, capabilities, and server identity. (#276)- An MCP request naming an unserved protocol version is rejected with
-32022listing what is served. (#276) Query(group=True)reads a model annotation's fields from the query string. (#274)group=Trueis accepted byHeader,Cookie, andFormfor the same field spread. (#274)SessionAuthresolves a cookie session into the request'sPrincipal. (#274)login_sessionandlogout_sessionsign a subject in and out, rotating the session id. (#274)HealthPluginserves/livezand/readyz, failing readiness once shutdown begins. (#274)app.add_lifespan()registers additional lifespan context managers on the app's exit stack. (#274)
Changed¶
Request.content_lengthreads the raw header tuples instead of materializingHeaders. (#275)
Fixed¶
- An
HTTPExceptionreports the same body over MCP and background tasks as it does over HTTP. (#274) - An MCP tool call with invalid arguments is reported in-band as
isError, not as a protocol error. (#277) - An MCP argument-validation message names the offending argument instead of rendering a Python repr. (#277)
[0.14.0] - 2026-08-19¶
Added¶
requestandcsp_nonceresolve in templates without threading them through the render context. (#272)csp_nonce()reads the request being handled when called without one. (#272)
Changed¶
instrument_with_prometheusreports a collector-name collision with theregistry=andprefix=fixes. (#272)
[0.13.0] - 2026-08-19¶
Added¶
ws.appexposes the serving application on a WebSocket, mirroringrequest.app. (#269)RateLimitMiddleware(strict_overrides=False)warns instead of failing on an override key matching no route. (#269)
Changed¶
WebSocketdeclares__slots__, cutting per-connection memory; attach data tows.state. (#270)SessionStoredeclares__slots__, so a slotted store subclass no longer carries a__dict__. (#270)
Fixed¶
- A parameter marker's default is applied when an MCP tool call omits the field. (#269)
- An MCP tool's
inputSchemaadvertises a parameter marker's declared default. (#269) X-RateLimit-Resetnever advertises a wait longer than the configured window. (#269)
[0.12.1] - 2026-08-16¶
Fixed¶
- A scalar
Body()parameter is documented in the OpenAPIrequestBodyinstead of omitted. (#267) Body(embed=True)params document one JSON object body, required only when a field is. (#267)- On Python 3.10, an
Annotated[T, Body()]parameter defaulting toNoneis read from the body, not the query string. (#267)
[0.12.0] - 2026-08-16¶
Changed¶
- A handler's return annotation now supplies
response_model, so the response is filtered to the annotated model. (#263) - Pass
response_model=Noneto keep a model return annotation while declaring no response contract. (#263)
Added¶
veloce checkreports routes with no response schema and routes whoseresponse_modelcontradicts the return annotation. (#263)app.response_contract_audit()returns those findings for a pre-deploy script or test. (#263)- A
list[Model]return annotation documents an array response and filters its elements. (#264) - A union return annotation (
A | B,A | None) documents its alternatives asoneOf. (#264) Veloce(debug=True)logs the response-contract findings at startup. (#264)
Fixed¶
- A
response_modelsubclass instance is re-shaped to the declared model instead of leaking its extra fields. (#263)
[0.11.0] - 2026-08-03¶
Added¶
app.install(plugin)registers an app extension in one call — any object with aninstall(self, app)method. (#253)name, when set on a plugin, records it underapp.extensions. (#253)- The
/docsand/redocpages carry the CSP nonce on every script, style, and stylesheet tag. (#260)
Fixed¶
- A response emits one header line per field name, so two casings of one header no longer both ship. (#260)
RateLimitMiddlewarerejects an override key matching no route at startup, not on every request. (#260)veloce customprints the app's CLI group help instead of crashing when no command is given. (#254)
[0.10.0] - 2026-07-06¶
Added¶
@app.queryregisters a route for the HTTPQUERYmethod (RFC 10008) — safe and idempotent likeGET, with a request body likePOST. (#244)
[0.9.0] - 2026-06-24¶
Added¶
SecuritySchemeis the shared base for authentication schemes, owningauto_errorand the__call__(request)contract. (#236)stream=Trueon a route opts its handler into incremental request-body reading viarequest.stream(), instead of buffering the body first. (#222)MCPErrorand typed subclasses (InvalidParamsError,AuthorizationError, others) let an MCP handler raise a specific JSON-RPC error. (#229)- The MCP HTTP transport rejects an unsupported
MCP-Protocol-Versionheader with400. (#230) ProtocolVersionErrorandOriginNotAllowedErrorsurface MCP transport violations as typed errors. (#230)- The MCP HTTP endpoint answers a
GETwith405 Method Not Allowed. (#230) - The MCP SSE stream sends a priming event on open and a
retryfield before closing. (#230) - The MCP
initializeresult emitsinstructionsfrom the app description or summary. (#230) - The MCP
initializeresult emits aserverInfo.titlefrom the app title. (#230) - MCP tool annotations now carry
openWorldHintand the route summary asannotations.title. (#230) - MCP tool
inputSchemaandoutputSchemadeclare the JSON Schema 2020-12 dialect. (#230) Iconobjects on@app.mcp_tool,@app.mcp_prompt, andmcp_icons=routes surface as a primitive'siconsarray. (#230)- MCP content blocks carry optional
audience/priority/lastModifiedannotations. (#230) ResourceLinkandEmbeddedResourcecontent blocks let a route return a linked or inlined resource result. (#230)@app.mcp_completeranswers MCPcompletion/completewith per-argument value suggestions for a prompt or resource. (#230)- MCP
notifications/cancelledcancels the named in-flight request and unwinds its task. (#230) MCPSessionrecords the client capabilities advertised ininitializeover the stdio transport. (#230)MCP_ENFORCE_LIFECYCLErejects a request that precedesinitializeon a stateful connection. (#230)task_support=Trueopts an MCP tool into a task-augmentedtools/callthat runs in the background. (#230)- A task-augmented
tools/callreturns aCreateTaskResultthe client polls for the result. (#230) tasks/get,tasks/result,tasks/list, andtasks/canceldrive an MCP task through its lifecycle. (#230)- The MCP server emits
notifications/tasks/statuswith the related-task_metaon each task transition. (#230) mount_mcp(transport="http", sessions=True)assigns and validates anMcp-Session-Idon the HTTP transport. (#230)- The MCP HTTP transport rejects a missing required session id with
400and a terminated one with404. (#230) - A
DELETEon the MCP HTTP endpoint terminates the session when session management is enabled. (#230) SessionRequiredErrorandSessionNotFoundErrorsurface MCP session violations as typed errors. (#230)mount_mcp(transport="http", resumable=True)attaches per-stream ids to MCP SSE events and keeps a bounded replay buffer. (#230)- A
GETcarryingLast-Event-IDresumes an MCP SSE stream, replaying only that stream's missed events. (#230) MCP_RESOURCE_SUBSCRIPTIONSlets a clientresources/subscribeandresources/unsubscribeto a resource URI. (#230)MCPServer.notify_resource_updatedsendsnotifications/resources/updatedto subscribed connections. (#230)MCPServer.notify_resources_list_changedsendsnotifications/resources/list_changedto open connections. (#230)MCPContext.sampleasks the client's model for a completion viasampling/createMessage. (#230)MCPContext.elicitrequests user input viaelicitation/createin form or URL mode. (#230)MCPContext.rootslists the client's filesystem roots viaroots/list. (#230)- The stdio transport issues server-to-client requests and awaits their correlated replies. (#230)
MCPCapabilityErrorrejects a server-initiated request the client did not advertise support for. (#230)- The MCP
resourcescapability advertisessubscribeandlistChangedwhen subscriptions are enabled. (#230) - MCP resource subscriptions deliver
notifications/resources/updatedover a stateful HTTPMcp-Session-Idconnection. (#230) - The MCP HTTP transport records the client capabilities from
initializeon a session, gatingMCPContext.sample/elicit/roots. (#230)
Changed¶
- A client disconnecting from an MCP SSE stream no longer cancels the in-flight call. (#230)
MCPContext.cancelledreflects real cancellation state instead of always returningFalse. (#230)- The MCP HTTP transport advertises
resources.subscribe/listChangedastrueonly withsessions=True; a stateless request advertisesfalse. (#230) MCP_ENFORCE_LIFECYCLEis enforced on a stateful HTTPMcp-Session-Idconnection, not only over stdio. (#230)Response.mimetype,charset, andmimetype_paramscache their parse, keyed on the currentcontent_typevalue. (#239)- Route registration rejects a path parameter name that is not a valid Python identifier or is a reserved keyword, instead of failing opaquely at request time. (#240)
Fixed¶
- Response and DI-injected background tasks are tracked and cancelled-and-drained on shutdown, so one no longer outlives the event loop and is orphaned mid-run. (#241)
request.json()caches a JSONnullbody asNoneso it is parsed once instead of re-decoded on every access. (#240)- The MCP HTTP
GETresume path validatesOriginandMCP-Protocol-Versionso a cross-origin or unsupported-version client cannot bypass the DNS-rebinding defense. (#237) - MCP
completion/completebounds the number of client-suppliedcontext.argumentsentries it ingests. (#237) - A malformed inbound
traceparentno longer raises out of the OpenTelemetry span-emit hook; the span is rooted instead. (#237) - An MCP task that settles after a racing
tasks/cancelkeeps itscancelledstatus instead of being overwritten. (#237) - MCP
notifications/cancelledignores a non-scalarrequestIdinstead of raisingTypeErroron the lookup. (#237) PlainTextResponseandHTMLResponsenow acceptbytesas well asstr, matching Starlette parity. (#226)- An MCP HTTP client's
notifications/cancelledcancels only its own in-flight request, never a peer's call with a colliding JSON-RPC id. (#230) - An MCP task is private to the connection that created it;
tasks/listandtasks/get/result/cancelreject another connection's task. (#230) - The MCP HTTP session store evicts idle
Mcp-Session-Idsessions so an abandoned session no longer leaks for the process lifetime. (#230) - The MCP SSE event store caps retained streams so a long-running resumable server's replay buffer no longer grows without bound. (#230)
- An MCP task keys ownership to a stable per-connection id so a task cannot alias to a later session that reuses a freed session's address. (#230)
- Evicting an MCP HTTP session cancels and drops its tasks so a never-settling task no longer pins memory for the process lifetime. (#230)
tasks/canceldelivers itsnotifications/tasks/status(cancelled) reliably instead of dropping it to garbage collection. (#230)- Concurrent MCP SSE streams on one
Mcp-Session-Ideach receive resource-update notifications and unregister independently. (#230) mount_mcp(transport="http")rejects atask_supporttool withoutsessions=Trueso a created task is never silently unretrievable. (#230)- An MCP task runner refuses
ctx.sample/elicit/rootson stdio, settling the task failed instead of racing the serve loop's reader. (#230)
[0.8.0] - 2026-06-13¶
Added¶
Veloce.run(reload=True)andveloce run --reloadauto-restart the built-in server on source changes, without uvicorn. (#212)EVENT_LOOP_WATCHDOGnames the route and dependency a blocking call stalled in. (#210)
[0.7.0] - 2026-06-12¶
Changed¶
- OpenAPI parameters derive from the handler plan the resolver runs, keeping documented and enforced contracts in lockstep. (#205)
clickis now an optionalcliextra; installveloceframework[cli]to useapp.cliandtest_cli_runner. (#206)
Fixed¶
- Parameters the resolver treats as optional are documented as
required: false, matching runtime. (#205) - A form request body whose every field is optional is documented as not required, matching runtime. (#205)
FileResponse.from_pathemits a bareContent-Dispositionfor a non-default disposition with no filename, matching the sync constructor. (#207)
[0.6.0] - 2026-06-10¶
Added¶
veloce new NAME [--template minimal|api|web]scaffolds a project, andveloce generate KIND NAME(aliasg) emits a single file. (#197)get_flashed_messagesis auto-injected as a Jinja global, so templates call it without manual registration. (#197)SessionMiddleware/ServerSessionMiddlewareresolve unset constructor arguments fromapp.configon the first request. (#198)app.secret_keyis a live property bound toconfig["SECRET_KEY"], so it alone configuresSessionMiddleware. (#198)send_file/async_send_fileapplySEND_FILE_MAX_AGE_DEFAULTwhen called withoutmax_age=. (#198)
Security¶
CORSMiddleware(allow_origin_regex=...)gates strictly by the regex instead of defaultingallow_originsto["*"]. (#197)
Changed¶
APIRouternow aliasesRouter(wasBlueprint); constructBlueprintfor a named route group. (#198)MAX_CONTENT_LENGTHdefaults to104857600(100 MiB); set it toNonefor unlimited. (#198)- A failing
yield-dependency teardown now reachesgot_request_exceptionand re-raises underPROPAGATE_EXCEPTIONS. (#198)
Fixed¶
/docsrenders withBaseLayoutinstead of the unloadedStandaloneLayout. (#197)@rate_limitis honored oninclude_in_schema=Falseroutes (with the strategy API). (#197)@app.endpoint(name)reclassifies the route so a sync view is offloaded, not awaited. (#198)PROPAGATE_EXCEPTIONS=false(and0/off) from an env file now reads as off. (#198)security_audit()no longer claims session signing falls back to weak defaults whenSECRET_KEYis unset. (#198)- The native server drops chunked-request trailer fields instead of prepending them to the next request. (#198)
- A mounted sub-app's trailing-slash redirect carries the mount prefix in its
Location. (#197) - A non-ASCII
query_stringover ASGI returns400instead of raising a500. (#197) - A
multipart/form-databody that fails mid-parse returns400, not a partial200. (#197) StreamingResponseon the native server no longer truncates on an emptybyteschunk. (#197)- Registering
/usersand/users/no longer flips the first to a slash redirect. (#197) - Blueprint routes keep
exclude_middleware=[...]afterregister_blueprint. (#197) - A mutable parameter default (
tags: list[str] = []) is no longer shared across requests. (#197) ProxyFixkeeps the brackets and port of aForwardedIPv6host. (#197)- A native-server
HEADresponse no longer sends a body, keepingContent-Length. (#197) - The native server no longer drops a WebSocket frame pipelined into the handshake segment. (#197)
- A non-WebSocket
Upgrade(e.g.h2c) returns400without running the route handler. (#197)
[0.5.0] - 2026-06-10¶
Added¶
- MCP HTTP transport hardening:
mount_mcp(transport="http", allowed_origins=[...])validates theOriginheader (DNS-rebinding defense), andexclude_middleware=[...]drops named app middleware from the/mcp+ metadata routes (so an app-wide auth middleware the transport's ownauthreplaces does not run on it). (#194) - MCP authorization:
mount_mcp(transport="http", auth=MCPAuth(...))makes the endpoint an OAuth 2.1 resource server — a user-suppliedverifycallable validates the bearer token on every request, the RFC 9728 protected-resource metadata is served, and a missing/invalid token returns401(insufficient endpoint scope returns403) with aWWW-Authenticatechallenge. Declarative per-tool scopes (@app.mcp_tool(scopes=...),mcp_scopes=on exposed routes) are enforced against the request principal. (#194) Principal+current_principal()/set_principal(): a unified authenticated identity populated by HTTP auth or the MCP transport, so authorization and identity-aware dependencies read one source across both doors. (#194)Request.is_mcpmarks a replayed MCP tool/resource call, so auth middleware can defer to the transport on agent calls while business middleware runs unchanged. (#194)- MCP Streamable HTTP transport:
app.mount_mcp(transport="http", path="/mcp")mounts the MCP server as aPOSTroute, so it can run as a remote/hosted server under any ASGI server. A request withAccept: text/event-streamis answered with an SSE stream of the call's progress/log notifications followed by the JSON-RPC response; otherwise a single JSON response. The route is protected by whatever middleware and dependencies the app applies to it. (#194) - MCP progress and logging:
MCPContext.report_progress(...)andMCPContext.log(...)now send livenotifications/progressandnotifications/messageto the client (progress requires the client'sprogressToken); the server handleslogging/setLeveland advertises theloggingcapability. (#194) - MCP per-call timeout: set
app.config["MCP_CALL_TIMEOUT"](seconds) to bound each tool call, resource read, and prompt render; an overrun is cancelled and surfaced as an in-band tool error or a JSON-RPC error. Unset (no timeout) by default. (#194) - MCP prompts: register a reusable prompt template with
@app.mcp_prompt(...). The callable's parameters become the prompt's arguments and its return (a string or a list of role/content messages) becomes the rendered messages; the server answersprompts/listandprompts/get, withDepends/MCPContextresolved as in a tool, and advertises thepromptscapability when at least one is registered. (#194) - MCP resources: expose a read-only (
GET/HEAD) route as a Model Context Protocol resource withexpose_as_mcp_resource=Trueandmcp_resource_uri=...(a static URI, or a URI template such asusers://{user_id}binding the route's path parameters). The server answersresources/list,resources/templates/list, andresources/read, replaying the route's dependencies, security, andresponse_modelthrough the shared invocation path; it advertises theresourcescapability when at least one resource is registered. (#194) - MCP non-text tool content: a tool returning an
image/*oraudio/*response emits the matching typed MCP content block (base64), and a binary resource read returns its bytes as ablob. (#194)
Fixed¶
- The native dev server (
app.run()) now starts on Windows:reuse_portis requested only whereSO_REUSEPORTexists, instead of unconditionally passingreuse_port=Trueto the selector event loop (which raisedValueErrorand killed the serving thread before it bound). (#195) - The native dev server now drains in-flight requests on shutdown on Windows too:
where
loop.add_signal_handleris unavailable,_servefalls back tosignal.signaland schedules the cooperative shutdown on the loop, so Ctrl+C / Ctrl+Break let an in-flight request finish at its boundary instead of raisingKeyboardInterruptstraight out of the loop and resetting the connection. (#195) - Blueprint error handlers are now scoped to their own routes: a
@bp.errorhandleronly catches exceptions raised on that blueprint (or a nested descendant), consulted by the failing request's blueprint chain before the app-level handlers — it no longer catches a sibling blueprint's or an app-level route's exception.error_handler_specnow reports per-blueprint sub-tables. (#195) - A mounted Veloce sub-app now sees
request.root_path(andscript_root) set to its mount prefix, matching mounted ASGI apps, sourl_forand proxy-aware URLs inside the sub-app are prefix-correct. (#195) JSONResponse,HTMLResponse, andPlainTextResponseacceptbackground=(forwarded to the baseResponse), so aBackgroundTask/BackgroundTaskscan be attached to them as it can toResponse. (#195)FileResponse(content_disposition_type="inline")now emitsContent-Disposition: inlineeven without afilename; an explicit non-default disposition is honoured (the defaultattachmentwithout a filename still emits no header, so plain file responses are not forced to download). (#195)- The
sessionproxy forwards attribute writes, sosession.permanent = Trueworks through the global proxy rather than raisingAttributeError. (#195) - A single Pydantic body model's validation errors are now located under
"body"(e.g.["body", "field"]), consistent withBody(...)marker params and the whole-body error cases. (#195) - MCP: the
logging/setLevelminimum is now scoped per request (a ContextVar like the progress/notification channel) rather than on the sharedMCPServer, so one HTTP client's level change no longer raises the notification floor for others. (#194) - MCP: a resource read short-circuited by an auth guard (
401/403) maps to a forbidden error rather than an internal error. (#194)
Security¶
- MCP: a pure
@app.mcp_toolhandler error (and the defensive internal-error path) surfaces a generic message unlessapp.debugis set, so an exception carrying a secret is not returned verbatim to the agent. (#194) - MCP: a tool argument can no longer masquerade as an
Authorization/Cookieheader on the replayed request, so aSecurityscheme cannot read agent-supplied input as a credential;Principal.tokenis excluded fromrepr();MCPAuthrequiresresource_server_url+authorization_servers; and an insufficient scope is reported uniformly across tools/resources/prompts (HTTP 403 with aWWW-Authenticatechallenge over the JSON transport). (#194)
[0.4.0] - 2026-06-08¶
Added¶
- Configurable rate limiting: selectable algorithms (
FixedWindow,SlidingWindow,TokenBucket), pluggable in-memory or Redis backends, and per-route limits viaoverridesor the@rate_limitdecorator. - Result caching: the
cacheddecorator withInMemoryCacheandRedisCache. (#171) veloce.contrib.redis:RedisSessionStore,RedisRateLimitBackend, andRedisCachefor state shared across workers.- msgspec as an opt-in fast validation and serialization backend. (#157)
- Model Context Protocol integration (
veloce.contrib.mcp): tool exposure over stdio, protocol-version negotiation,ping, route-derived tool metadata, and streaming-result tools. - JSON Web Tokens (
encode_jwt/decode_jwt), storage-free reset tokens (make_reset_token/check_reset_token), and aSecretwrapper that resists accidental disclosure. (#139) CSPMiddleware(Content-Security-Policy with a per-request nonce and report-only mode) andConditionalGetMiddleware(304forIf-None-Match/If-Modified-Since). (#139)CORSMiddlewaregains Private Network Access support and preflight-method validation;CSRFMiddlewaregains Origin verification viatrusted_origins. (#136)- Middleware ordering with
add_middleware(..., priority=N)and per-route opt-out withexclude_middleware=[...]. - Background-task supervision:
app.supervise(...)(restart policy) andapp.spawn(...)(app-scoped tasks). - Routing: constrained converter syntax (
{x:converter(arg)}),date/time/ decimal path converters, duplicate-route detection, and the declarative@app.websocket_listenerroute. StaticFiles: precompressed-sibling serving,html=Truedirectory indexes, and write-sideIf-Match/If-Unmodified-Sincepreconditions.- WebSockets: native-transport server support on
Veloce.run(), an idle-receive timeout, async-context-manager support, heartbeats, send backpressure, and UTF-8 / close-frame validation. - Server-Sent Events:
ServerSentEvent.commentand.json, bare-value source iterators, and a proactive heartbeat. - Observability:
instrument_access_log/log_requests_as_json, a Prometheus exporter (instrument_with_prometheus), and an OpenTelemetry bridge with a live-tracing mode and anon_spanhook. - OpenAPI: separate request/response schemas, identity-keyed components,
operationId de-duplication, a documented
422response, and avalidate_openapiflag. - Sessions: sliding expiry,
domain=/ chunked-cookie options, andVary: Cookieon cookie-varying responses. - Encoder extensibility: a per-call
custom_encoder, process-levelregister_encoder, and broader built-in coverage (bytes,set/frozenset,pathlib.Path,re.Pattern, scalar subclasses). - Deployment: optional gunicorn
VeloceWorker, built-in dev-server TLS, an ASGI-appmount,.envloading, a dev event-loop watchdog, and an asyncTestClient.uvicornis now an optional extra rather than a hard dependency. - New top-level exports:
Config,Aborter,URLRule,SetupError,JSONProvider/DefaultJSONProvider/config_orjson_options,get_openapi_schema/setup_openapi_routes,StaticFiles,Jinja2Templates,log_requests_as_json, andasync_send_file. - Developer documentation: a build-one-app tutorial, a runnable
examples/directory, a databases guide, and a Hypothesis fuzzing harness across the parsers, router, signing, and WebSocket paths.
Changed¶
- The deprecated
Veloce.on_event()/Veloce.add_event_handler()now target removal in1.0.0. (#173) Veloce.run(workers=...)raisesValueErrorfor any worker count other than1(the built-in server is single-process). (#166)- Independent dependencies resolve concurrently, and a no-wave
Dependschain compiles to a straight-line async resolver. (#154) - Numerous per-request and schema-generation paths were optimized — a compiled feature pipeline, indexed route/encoder lookups, and bounded caches — without changing public behavior.
- Route resolution gates its mounted-app, static-handler, and ASGI-mount scans on the compiled pipeline flags, skipping each scan when nothing of that kind is registered. (#183)
- Literal request paths resolve through a registration-time exact-match map in one
hash lookup instead of a radix-tree walk, falling through to the tree for
parameterized, wildcard, and slash-redirect routes (literal
match()~1.7x faster, ~3x on deep literal paths). (#185) - Requests to feature-free apps take a straight-line dispatch fast path: when no
middleware, request/response hooks, mounts, or url-value preprocessors are
registered and the matched route is an async trivial or request-only handler
with no response model, custom response class, non-default status, host or
subdomain constraint, defaults, or middleware exclusion, the middleware, hook,
route-resolution, and dependency-resolution orchestration is skipped while
coercion,
after_this_requestcallbacks, background tasks, exception handling, and teardown remain shared (~6-8% lower per-request dispatch time on those routes, in-process A/B). (#185)
Fixed¶
- Per-route rate-limit state now rebuilds when routes are added after startup. (#178)
- A bodiless status (
1xx,204,205,304) no longer advertises a body, the WebSocket handshake uses the correct RFC 6455 GUID, and a frame with a non-zero RSV bit is rejected. HTTPBasic/HTTPDigestescape therealm, non-latin-1 header values are RFC 2047 encoded, anddecode_jwtrejects an empty secret.- JSON serialization handles
set/frozenset,pathlib.Path, integer-valuedDecimal, andexclude_none;StaticFilesprecompressed selection returns406and honours an explicitq=0. - Assorted correctness fixes across OpenAPI dual-schema comparison, scope-aware
dependency caching,
instrument_with_otelidempotency, signal delivery, and per-route middleware-exclusion symmetry.
Security¶
LoggingMiddlewareand the access log escape control characters in request-derived fields (CWE-117 log forging), andRequestIDMiddlewaresanitizes an inbound request id.- Security headers are matched case-insensitively so a handler override is not
silently replaced; the cookie writer round-trips a literal
%. - The native WebSocket server rejects an unmasked client frame, HTTP Basic
rejects an RFC 7617-malformed credential, and
dump_cookierejects a non-token cookie name. safe_joinrejects Windows reserved device names,URL.from_requestvalidates theHostheader (RFC 3986), and the router rejects a path that binds one parameter name twice.
[0.3.0] - 2026-06-01¶
Fixed¶
StaticFilesnow applies RFC 9110If-Rangevalidation correctly before serving partial responses. (#128)
[0.2.0] - 2026-05-31¶
Added¶
- Streaming request bodies on the built-in HTTP server, so large uploads no longer require buffering the full body before dispatch. (#106)
- CLI plugin discovery,
.envloading, template streaming, SSE heartbeat support, OpenTelemetry integration, and a signal namespace helper. - Hybrid routing for patterns that do not fit the radix tree, plus an optional gunicorn worker.
- Broader documentation coverage across configuration, templates, static files, sessions, signals, and related framework guides.
Changed¶
- Request body access is now asynchronous:
request.body(),request.text(), andrequest.get_data()must be awaited. (#106) request.stream()now streams on the raw HTTP path instead of replaying an already-buffered body. (#106)- Debug mode renders an HTML traceback page for clients that prefer HTML while preserving plain-text tracebacks for CLI and programmatic clients. (#117)
- Resolver and response-encoding internals were consolidated and optimized without changing the public API.
Fixed¶
- Correct handling for
If-Range, partial-content gzip behavior, async template context processors, duplicate response headers, hybrid-router edge cases, and gunicorn worker lifecycle/TLS behavior.
Security¶
- Restored strict header validation on streamed responses.
- Applied the same form-field limits to URL-encoded bodies as multipart forms.
[0.1.4] - 2026-05-25¶
Changed¶
- Focused maintenance release covering security hardening, correctness fixes, API cleanup, and small internal consolidations.
- Improved encoder behavior, cached more parsed request metadata, and reduced duplicated logic across middleware, CLI helpers, templating, and the test client. (#95)
Security¶
- Tightened multipart UTF-8 validation,
HTTPBasicchallenge construction, and exception handling around basic-auth parsing. (#95) - Made HSTS subdomain coverage opt-in rather than implicit. (#95)
Removed¶
- Dropped unused internal constants from the handler-plan implementation. (#95)
[0.1.3] - 2026-05-23¶
Changed¶
- Security and correctness release covering CSRF token rotation, password-hash parameter validation, and several framework/runtime fixes.
- Improved diagnostics around OpenAPI schema generation and clarified the process-local scope of the built-in rate limiter. (#94)
Fixed¶
- Addressed loop-affinity issues in
Veloce(), multipart encoding in the test client, stale response-encode caches, router merge behavior, and several runtime guards that previously relied onassert. (#94)
Security¶
- Added CSRF token rotation support after login or privilege changes. (#94)
- Rejected weak or tampered scrypt parameters during password verification. (#94)
- Added SRI protection for Swagger UI and ReDoc assets. (#94)
[0.1.2] - 2026-05-23¶
Added¶
- Top-level exports for
render_template,render_template_string, andJinja2Templates. (#78)
Changed¶
Request.json()became asynchronous for consistency with the rest of the request-body API. (#78)- Runtime dependencies were corrected so standard installs include the pieces needed for documented framework features. (#78)
veloce.__version__now comes from installed package metadata. (#78)
[0.1.1] - 2026-05-23¶
Changed¶
- Metadata-only release correcting maintainer information in the published package.
[0.1.0] - 2026-05-23¶
Added¶
- Initial public release of Veloce as
veloceframework. - Core framework surface including the
Veloceapp, radix-tree routing, request/response primitives, dependency injection, OpenAPI generation, and an in-memoryTestClient. - Built-in middleware, sessions, templating, signals, background tasks, Server-Sent Events, WebSockets, security helpers, and class-based views.
- CLI commands, static-file support, instrumentation hooks, server-side sessions, async password helpers, and the first round of performance-focused hot-path improvements.
Changed¶
- Set safer defaults and improved consistency across response handling, multipart uploads, WebSocket dependency injection, and request streaming.
Fixed¶
- Corrected early issues in blueprint registration, SSE encoding, dependency coercion, multipart cleanup, session-store race handling, static-file caching, logging, and request-scoped resource cleanup.
Security¶
- Added incremental request-size enforcement, request timeouts, WebSocket origin checks, security headers, signed CSRF tokens, multipart limits, and secure deployment audit helpers.