Repository navigation
v0.7.0
Upgrading
- Run
kit:update plan|applyformcp(re-pinnedsha256:29d14952…750e→sha256:e35e2879…7ae0):Mcp/Authorize/Page.vuetakes the "Also allow" grant checkboxes below. Nothing to do for an app with no explicit tool: the fieldset renders only when there is a grant to offer. - Run
kit:update plan|applyforshell(re-pinnedsha256:6b2a9dac…5077→sha256:93780271…035d): the manifest gains thevite_global_typeshost step below; no stub changes. - The kit's runtime floors are now
tey/mod ^0.11.6,tey/standards ^0.11.51,spatie/image ^3.9andlaravel/mcp ^1.0.2(see Fixed). An app on an older version of any of them gets the new one oncomposer update; nothing else to do. - Run
kit:update plan|applyformcp(re-pinnedsha256:7e095959…f01b→sha256:29d14952…750e):Mcp/Authorize/Page.vueshows the redirect host and the unverified-app note below, and the migration0001_01_02_000603_create_kit_mcp_registrations_tableis new. Thenphp artisan migrate. - The
mcpfeature now serves its own/.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resourcedocuments (root and path-suffixed) and no longer callsMcp::oauthRoutes(). A host that re-declared those routes over laravel/mcp's (to advertise its scopes) can drop them:scopes_supportedis the kit's token scopes, which include the host's. A host that re-declaredPOST /oauth/registerover laravel/mcp's controller can drop that too and setkit.mcp.registrationto true; its existing clients keep working.token_endpoint_auth_methods_supportedis now["none"]only. - A host's unregistered-
redirect_uritest that expected Passport's 401invalid_clientJSON for a browser request now gets the kit's 400 page; JSON requests keep the 401. Thelaravel/mcprequirement is^1.0.2. - A client that registered
http://localhost/...(orhttp://127.0.0.1/...,http://[::1]/...) now matches that redirect on any port at authorize and token time (RFC 8252 §7.3), as Claude Code's per-session port needs. Only the port is free: the scheme, host, path and query still match exactly, and nothing changes forhttpsredirects. - New migration for
mcp,0001_01_02_000603_create_kit_mcp_registrations_table(thekit_mcp_registrationstable: which OAuth clients came fromPOST /oauth/register). Runkit:update plan|applyformcp, thenphp artisan migrate. Until it is migrated, registration answers 500server_errorandkit:mcp:clientsrefuses. A client registered before (laravel/mcp's registration, or the kit's before this table) is not in it:php artisan kit:mcp:clients --adopt=<client id>records one by hand, so that--revokeand--prune-unusedcan touch it; the kit never guesses. - Run
kit:update plan|applyforcore(re-pinnedsha256:ea1d2e42…3b8c→sha256:652e0756…7ac3):Kit/Form/Amount.vueis new, andKit/Form/InputNumber.vue,Kit/Form/number.ts,Kit/contract.tsandKit/index.tschange for percent, symbols and the sharedkitNumberdefaults. - Money inputs no longer default to CAD. Set
KIT_CURRENCY=CAD(configkit.currency) to keep the old symbol, or passcurrencyper input. - Run
kit:update plan|applyforcore(re-pinnedsha256:652e0756…7ac3→sha256:ca87cd7a…7242):Kit/Form/AddressFieldset.vue,Kit/Form/AddressText.vue,Kit/Form/RegionSelect.vueandKit/Form/address.tsare new, andKit/contract.tsandKit/index.tsexport them. - Run
kit:update plan|applyforcore(re-pinnedsha256:ca87cd7a…7242→sha256:bc488ba6…e150):Kit/Form/InputTags.vueandKit/Form/tags.tsare new, andKit/contract.tsandKit/index.tsexport them. InputTags puts a caller'saria-describedby,aria-invalidandrequiredon its input, as InputSelect does. - Run
kit:update plan|applyforcore(re-pinnedsha256:bc488ba6…e150→sha256:a3439714…0e66): this addsKit/Data/DateTime.vue,dateTime.tsanduseTimezone.ts. - Run
kit:update plan|applyforcore(re-pinnedsha256:a3439714…0e66→sha256:962f4983…6ecb): this adds the pickers (Kit/Form/InputDate.vue,InputDateRange.vue,InputMonth.vue,InputYear.vue,InputTime.vue,InputTimeRange.vue,PeriodPicker.vueanddateInput.ts), andcss/kit.cssgains their phone-sheet rule.corenow declares@internationalized/date(^3.5.0, already installed with reka-ui) in its npm requirements. - Run
kit:update plan|applyforcore(re-pinnedsha256:962f4983…6ecb→sha256:a04c162c…2dd6):Kit/Layout/stickyBars.tstakes the fix below. A bar registered withregister(px)keeps working:pxnow means its inset from the viewport's bottom, which for a bar flush with the bottom is its height. - Run
kit:update plan|applyforcore(re-pinnedsha256:a04c162c…2dd6→sha256:81fceb81…95fd) andnotes(sha256:d373d438…f1b2→sha256:1a0455e7…551b):css/kit.cssadds the--success-text,--warning-textand--info-texttokens,Kit/Form/FormActions.vueshows "Saved" intext-success-text,Kit/Feedback/alertVariants.tsgives a pale Alert the theme's foreground for its text and a*-texttoken for its icon, and the Notes page'scolours.tsgives a coloured note the theme's foreground. No kit stub colours text with a raw palette colour any more. - A host with its own theme (not importing
kit.css) must define--success-text,--warning-textand--info-textfor light and dark; the kit's AA values are a starting point:hsl(142 76% 26%),hsl(26 90% 33%)andhsl(201 90% 32%)light,hsl(142 69% 58%),hsl(43 96% 56%)andhsl(199 89% 64%)dark. Until it does, the kit's theme host step reads unmet. A host theme's value wins over the kit's, as for the other tokens. - session-ui's confirm-password Cancel is now resolved by the bindable
Tey\Kit\Features\SessionUi\Contracts\ConfirmPasswordCancelUrl(defaultConfirmPasswordCancel). Nothing to do unless you want your own: bind it. No stub changes, no re-pin. - Run
kit:update plan|applyforcore(re-pinnedsha256:81fceb81…95fd→sha256:4a94fc7c…105e):Kit/Appearance/useAppearance.tstakes the storage fix below. No other stub changes. - Run
kit:update plan|applyforcore(re-pinnedsha256:4a94fc7c…105e→sha256:6537c1e1…3fa7):Kit/Workbench/Workbench.vuetakes the phone-tabs fix below. - Run
kit:update plan|applyforsession-ui(re-pinnedsha256:71617f5d…3108→sha256:592e21c4…7353):Auth/ResetPassword/Page.vuegains aFormStatus(below).
Added
kit:statusandkit:addflag an undefined connection ability.kit.mcp.connection_abilitydefaults tosettings.read, which the settings feature declares; a host without it (or with its own value) had every MCP connection refused aspermission_denied. With themcpfeature, the host requirements now carry akit.mcp.connection_abilityconfig step: met when a feature declares the ability through the Registry or the Gate defines it, else attention naming the key, the effect and the three fixes.test:statuscovers the default without settings, an undefined value, a Gate-defined one and a Registry-declared one.- Explicit grants for MCP tools (docs/mcp-consent.md › Explicit grants).
#[Capability(…, scope: 'x', explicit: true, explains: '…')]marks a tool that a blanket acts-as-me connection never covers: it is listed and callable only under a connection whose consent named its scope, and a call anyway is refusedscope_required(403), never reported as unknown. The kit's consent page offers each such scope the person may hold as an unticked "Also allow" checkbox showing the scope's label andexplains; only the tick grants (the OAuthscopeparameter never does), an existing connection never gains one (skipped consent and refresh keep its scopes), and the approval's audit entry records the grants.scopemust be a registered token scope andexplainsis required; registration refuses otherwise. On the AI assistants page each grant has its own Remove action (DELETE …/connections/{id}/grants/{scope}, auditedworkspace.connection.grant_removed), leaving the connection and its token in place.Registry::explicitScopes(),AuthorizeViewModel::grants(),WorkspaceConnection::explicit()andremoveGrant()are new.composer test:mcp-explicit-grants(34 checks). kit:statusandkit:addcheck that the Vite config gives @vitejs/plugin-vue the kit's types (theshellfeature'svite_global_typeshost step; README › Types). A page typed withdefineProps<Tey.Kit.…>()is resolved at build time fromscript.globalTypeFiles, so a production build fails withoutvendor/tey/kit/types/kit.d.tsthere; a tsconfig include alone serves the editor and vue-tsc. The Vite config (vite.config.ts,.jsor.mjs) is read as text, never run: the step is met when an array literal underglobalTypeFilesnames the kit's types (a plain path or apath.resolve(...)of it), unmet with the exact snippetvue({ script: { globalTypeFiles: ['vendor/tey/kit/types/kit.d.ts'] } })when the key is missing or the list lacks it, and "can't verify" (unmet, never passed) when there is no Vite config, it can't be read, orglobalTypeFilesis not an array literal. Attention, not blocking, like the other host steps. For a config the kit cannot read,kit.host.vite_global_types = 'manual'(KIT_HOST_VITE_GLOBAL_TYPES=manual) records that a person verified it; the step then reads "verified manually".composer test:statuscovers each case.kit.mcp.server.page_size(docs/mcp-tools.md › The server's name and instructions). How many tools, resources or prompts one MCP list answer carries, 50 by default (laravel/mcp's maximum; larger values are clamped, a non-number falls back to 50). laravel/mcp's default of 15 hid every tool past the fifteenth from assistants that never follownextCursor.composer test:mcp-server-configlists thirty tools in one page by default, pages at a host's 10, and clamps 500 to 50.- MCP resources from the host (docs/mcp-tools.md › Resources).
Registry::mcpResources([...])takes laravel/mcpResourceclasses or descriptors (uri,name,read, and optionallymimeType,description,title,capability), and the workspace server lists and serves them under the same admission as tools: a resource with#[Capability](usingGatedByCapability) or a descriptor'scapabilityis absent while that capability is off or out of plan, and a read of it anyway is refused with the reason (the kit'sresources/readmethod), not as unknown; a resource without one (docs) is always listed. A read runs inside the connection's workspace, and a descriptor's closure receives it. Refused at registration: a non-Resourceclass, a gated class without the trait, an undeclared capability, a descriptor missing its uri, name or read, a relative uri, a uri registered twice, or any of it without themcpfeature.composer test:mcp-resourcescovers the list, reads, the gate off and on, an unknown URI and each refusal. - MCP client registration and discovery, the kit's own (docs/mcp-registration.md). Opt-in OAuth 2.0 Dynamic Client Registration (RFC 7591) at
POST /oauth/register,kit.mcp.registration(KIT_MCP_REGISTRATION, off by default: the route does not exist), for public clients only: an assistant such as Claude registers itself with no secret issued and no management credentials. Redirect URIs are checked at registration:httpswith a host,httpto a loopback host on any port (Claude Code's per-session port, RFC 8252), or a private-use scheme the host lists inkit.mcp.redirect_schemes(empty by default); never a wildcard, fragment, userinfo, plainhttpelsewhere, orjavascript,data,blob,file. A per-IP rate limit (kit.mcp.registration_per_minute, 10) and an optionalkit.mcp.registration_policy(aRegistrationPolicyclass refusing with a reason, 403access_denied). The client is the Passport row laravel/mcp's registration stored, so a client registered before keeps working, with no column added. The discovery documents (RFC 8414 and RFC 9728, root and/mcp) are now the kit's, under laravel/mcp's route names, advertising only what the kit implements:code,authorization_codeandrefresh_token, S256, public clients, the kit's token scopes, andregistration_endpointonly when registration is on;Mcp::oauthRoutes()is no longer called. The 401 on/mcpaddsscope="mcp:use"to laravel/mcp's challenge. Passport's client bridge matches a loopbacklocalhostredirect without its port, as league already does for127.0.0.1.kit:mcp:clientslists registered clients,--revoke=<id>revokes one with its tokens,--prune-unused=<days>revokes those with no token in that time (--dry-runto look first).composer test:mcp-registrationcovers each RFC requirement, the refusals, the limit, the policy, the metadata shape, a pre-existing client and registration off. - The consent page names where the assistant sends you. It shows the redirect host Passport verified for the request, and for a self-registered client (dynamic client registration, or an owner-less public client shaped like one) adds "Unverified app": the name is the client's own claim, so a person can tell a lookalike from the app they expect.
AuthorizeViewModelgainsredirect_hostandregistered; theMcp/Authorize/Page.vuestub shows them. - PKCE S256 required at authorize. Every authorization request must carry a
code_challenge(43 to 128 unreserved characters) withcode_challenge_method=S256; a missing challenge or theplainmethod is refused with 400 before league sees it, never redirected. Claude and every MCP client send S256 already. - The OAuth error page, brandable. When an authorization request cannot go back to its app (unknown client, unregistered
redirect_uri, bad PKCE), a browser now gets a readable 400 page instead of Passport's bareinvalid_clientJSON; JSON callers and errors sent back to a verified redirect URI are unchanged. The kit'skit::oauth-errorview by default; a host setskit.mcp.oauth_error_viewto its own view, orOAuthErrorPage::using(fn (Request, string $error, ?string $description): Response)for its own page (an Inertia error page, say). A host that subclassed Passport's authorization controller for this can drop that. - Money and percent:
<InputNumber kind="percent">and<Amount>(docs/money-percent.md). Percentages are canonical in percentage points ("12.5" is 12.5 %). Money and percent inputs show the locale's symbol outside the editable text and name the unit in the label;negative="parentheses"gives the accounting form.<Amount>displays the same canonical strings in tabular figures. Locale and currency default to the new sharedkitNumber(configkit.currency, the app locale), then the browser. <AddressFieldset>,<RegionSelect>and<AddressText>(docs/address.md). A postal address as one named group, submitted asaddress[line1]…address[country], with autocomplete tokens, labels that follow the country (Province or territory and Postal code, State and ZIP code), Canadian postal codes written A1A 1A1 on blur, and non-blocking hints for unlikely CA and US codes. Regions are the Canadian provinces and territories and the US states and DC, named in English or French; other countries take free text.<AddressText>shows an address in an<address>element, with a map link only when the app passes its ownmapUrl. Labels come from the newaddress.*keys in the kit'suilang file.<InputTags>, free-text tags as chips (docs/input-tags.md). Built onInputSelect(multiple and creatable), so the combobox, chips and announcements are the same. A separator (,or;) or Enter adds the typed tag, and Enter takes a suggestion only after the arrow keys move to it. A paste with separators or line breaks adds each piece. Duplicates (ignoring case in the locale), tags overmaxLength, tags not matchingpatternand tags pastmaxare refused with the reason under the field, never trimmed. Submitted asname[]. Reasons come from the newtags.*keys in the kit'suilang file.DateTime, dates and times for the viewer (docs/date-time.md). It renders a<time>with the machine value and the full date in itstitle, in the formatsrelative,local,date,time,ymdanddatetime. A calendar date (YYYY-MM-DD) is never converted between zones, so it never shifts across midnight or DST. An instant (RFC 3339 with its offset) shows in the viewer's timezone, and one without an offset is shown as given, never guessed.livere-renders relative times on one shared minute ticker.formatDateTime(),fullDateTime()anduseTimezone()are exported too. It usesIntlonly, with no date library.- session-ui shares
kitTimezone, the viewer's zone (thetzcookie, then the person's saved zone, then the workspace's, then UTC), through KitShares, so it survives a flush.DateTime, and the pickers to come, read it. - The adoption inventory knows
data.date-display(a newshows-datesfact). Hand-rolled timeago.js and moment components, andTimeAgo/DateRelative-style files, count against it. - Date and time pickers (docs/date-pickers.md):
InputDate,InputDateRange,InputMonth,InputYear,InputTimeandInputTimeRange, on reka-ui. Each submits plain ISO strings (2026-11-01,2026-11,2026,09:30; a range asname[start]andname[end]). A date, month, year or time is a wall value, never converted between zones;kitTimezoneonly decides today. The calendars use the APG date picker keys and the month and year grids the APG grid keys; on phones they open as a bottom sheet, and in a locked LockedForm they stay shut. A half-filled or reversed range blocks the native submit.toInstant()andfromInstant()join a date and time into an RFC 3339 instant with the viewer zone's offset (DST gaps move forward, repeated hours take the earlier moment). Tey\Kit\Validation\IsoDateandIsoRange, the server rules for those values:IsoDate::date(),month(),year(),time()andinstant()(an offset required) with inclusive bounds, andIsoRange::dates()andtimes()for{start, end}.- Adoption surfaces
form.date-pickersandform.time-input, with the factstakes-datesandtakes-times. - The adoption inventory reads Blade, Livewire and Filament apps (tey/standards 0.11.51 or later). Every template-tag signal reads Blade views as well as Vue (
"in": ["vue", "blade"]). Filament resources underapp/Filament/**count as hand-rolled through class references:TextInput,SelectandTextareaforform.inputs,FileUploadforform.files,TableandTextColumnfordata.table,DeleteActionforaction.confirmandNotificationforfeedback.toast, mirrored into thehas-forms,has-file-uploadsandhas-index-pagesfacts. A newhas-filament-destructive-actionsfact letsaction.confirmapply to a Filament app with no delete route. Blade views count forshell.workspace(the conventional app layout files),layout.page(<x-app-layout>,<x-layouts.app>),action.button(<button>,<x-button>and the Breeze button components),feedback.toast(toast, flash and action-message views) andappearance.theme(theme-toggle, dark-mode and appearance views).platform.timezonegains nothing: no signal kind expresses hand-rolled timezone handling in PHP or Blade without guessing.
Changed
- Money inputs no longer default to CAD. Without
currencyorkit.currency,<InputNumber kind="money">shows no symbol (two decimals, the same canonical minor units) and warns in development. SetKIT_CURRENCY=CADto keep the old symbol.
Fixed
- A workspace created in the same request is admitted.
IsKitWorkspacereadadmittedas null until the model was read back, so a workspace made and used in one request was refusedadmission_required. The trait now mirrors the column's default (true in the kit's migration) on an unsaved or just-created model whose table has the column, so it answers the way its row will; an explicitadmitted => falsestays false, and admission stays distinct from creation (a host whose adopted column defaults to false setsadmittedto false in its model's$attributes).test:workspace-guardcovers create-then-check. - Leaving a tenant no longer wipes Pennant's array store.
TenantBoundaryand theFlushFeatureCacheTaskswitch task flushed Pennant's cache on every switch; under thearraystore (the kit's default, and tests) that cache is the store itself, so a flag deactivated earlier in the request came back on, and a test that deactivated a capability then called/mcpsaw it on. Both now flush only for a store with a backend (TenantBoundary::flushes(): the default store's driver is notarray), where the cache can go stale across tenants.test:capability-guardandtest:tenant-taskscover both stores. - A narrow connection no longer sees tools it cannot call.
GatedByCapabilitylisted tools by capability and role ability only, so a connection whose scopes coveredsettings.readalone still saw the write tools and gotconnection_deniedon the call. Presence now also requires that the calling connection covers the ability the tool is admitted under, the sameWorkspaceConnection::covers()the call path applies, sotools/listexactly matches what is callable; the refusal codes for a call made anyway are unchanged (connection_denied, orscope_requiredfor an explicit tool). Covered bytest:mcp-explicit-grants. - A long footer or inspector scrolls inside its phone tab. Below lg the Workbench's footer and inspector tabs rendered their slot straight into the tab's row, with no scroll container, so a long speaker note ran on behind the pinned actions bar and its end could not be reached. Both now sit in a
PaneScroll, as their panes do at lg and up. The browser suite checks each at 390×844: the tab scrolls, and after scrolling to the end the last line is fully visible above the bar. - The reset-password page shows a form status like its siblings.
Auth/ResetPasswordhad noFormStatus, whereAuth/ForgotPassword,Auth/LoginandAuth/VerifyEmaildo. It now has one, first inside the form as on ForgotPassword, showing thestatuspage prop when a host app supplies one. - Dependency floors are tested. CI's
testsjob gains alowestentry (PHP 8.4,composer update --prefer-lowest --prefer-stable) running every check against the oldest versionscomposer.jsonallows, so a requirement whose floor predates an API the kit uses fails in CI rather than on a host. Its first run raisestey/modto^0.11.6(0.11.0 to 0.11.5 lack the rollback fixtest:adoptneeds) alongsidelaravel/mcp: ^1.0.2, and addsspatie/typescript-transformer: ^3.3to require-dev (bin/kit-types.phpuseswithoutManifest(), new in 3.3.0, which laravel-typescript-transformer ^3.3 alone does not guarantee),symfony/event-dispatcher-contracts: ^3.5andsymfony/polyfill-php83: ^1.32(older ones emit PHP 8.4 implicit-nullable deprecations at boot, which the checks that require a silent boot read as failures), andspatie/image: ^3.9in require (laravel-medialibrary ^11.13 allows 3.3, whose WebP fit does not keep the declared widthtest:media-conversionsexpects; 3.9.0 is the first that does). - FormActions' "Saved" meets WCAG AA. It was
text-green-600, about 3.3:1 on a white card (AA is 4.5:1), so axe'scolor-contrastfailed on every FormCard just after a save. The new--success-texttoken, with--warning-textand--info-textbeside it, is status text on a surface, as--destructive-textis:hsl(142 76% 26%)light (5.80:1 on background, card and popover, 5.32:1 on accent and muted) andhsl(142 69% 58%)dark (11.36:1, 8.68:1 on accent, 8.35:1 on muted); warning is 6.04:1 and 11.70:1, info 6.06:1 and 9.71:1, and at least 5.32:1 light and 7.13:1 dark on accent and muted. A pale Alert's icon takes these tokens (and--destructive-textfor danger) instead of raw palette colours, and its text, like a coloured note's, takes the theme's foreground.tests/js/semantic-text.test.mjscomputes the ratios fromkit.cssand refuses a raw palette text colour (text-green-600,dark:text-sky-300) in any kit resource, and the browser job's?form-savedpage runs axe'scolor-contrastover a saved FormCard and the pale Alerts, light and dark. - A bottom toast clears a pinned bar that the layout holds above the viewport's bottom. The sticky-bar registry recorded each bar's height, and the Toaster lifted bottom toasts by that much. A filling Workbench's actions bar sits wherever the layout puts it, which under a shell shorter than its 8rem allowance is above the bottom edge, so a bottom-right toast landed on its right-hand buttons (Present, Save). Each bar now registers its inset (the distance from the viewport's bottom to its top), re-measured on scroll and window resize, and toasts clear the largest one. A browser test asserts that no bar button is under a toast, checking each button's left, centre and right, at 1440×900 with bottom-right toasts and at a phone width.
browser/andtypecheck/ignore a symlinkednode_modules(the pattern no longer ends in/, which matched directories only), so a contributor's linked install can't be committed. The resource checks refuse a tracked symlink.- session-ui's login page now shows Concierge's one-click local sign-in. The login view passed no
localLogin, so the button never appeared; it now carriesLocalLogin::offer(), which stays the only gate (null outsidelocal, and without Concierge). - Confirm-password Cancel falls through instead of jumping home. A refused
cancelquery (off the app, protocol-relative, backslash,javascript:, credentials, encoded) now falls to the referer, then to the intended URL one level up when a real non-guest route answers there, thenkit.home. kit:statusflags a missingkit.homeroute. With session-ui and no workspace-ui, sign-in and Concierge's no-workspace redirect land onkit.home; with no route answering GET there it now reports attention instead of letting sign-in 404.- Blocked browser storage no longer leaves the page blank.
useAppearanceread and wrotelocalStorageunguarded (ininitializeTheme, on mount and inupdateAppearance), so where storage throws on access (site data blocked, some private modes) the app never mounted. Every read and write is now guarded: a failed or unrecognised read falls back tosystem, and a failed write is ignored (theappearancecookie still carries the choice). The other core stubs that use storage (ComputedValueScope.vue,SplitPane.vue/splitState.ts,DataTable.vue,Workbench.vue/workbenchLayout.ts) were already guarded. The browser suite mounts the app withlocalStorageandsessionStoragethrowing.
What's Changed
- Add percent inputs and Amount for money and percent display by @jaspertey in #374
- Add AddressFieldset, RegionSelect and AddressText by @jaspertey in #377
- Add InputTags for free-text tags as chips by @jaspertey in #384
- DateTime: dates and times for the viewer; session-ui shares kitTimezone (2737 item 2) by @jaspertey in #387
- Pickers: InputDate, InputDateRange, InputMonth, InputYear, InputTime, InputTimeRange by @jaspertey in #393
- Sticky bars register their inset, so bottom toasts clear a bar held above the viewport's bottom by @jaspertey in #428
- Offer local sign-in, resolve confirm-password Cancel through a contract and flag a missing kit.home route by @jaspertey in #429
- MCP dynamic client registration (RFC 7591), kit-owned discovery metadata, PKCE S256, brandable OAuth error page by @jaspertey in #406
- Host-registered MCP resources on the workspace server, gated like tools by @jaspertey in #410
- CI: test the lowest allowed dependency versions; tey/mod ^0.11.6 by @jaspertey in #415
- kit.mcp.server.page_size: one tools/list page holds every tool by default by @jaspertey in #426
- kit:status and kit:add check plugin-vue's script.globalTypeFiles for the kit's types by @jaspertey in #421
- Explicit grants for MCP tools (opt-in scopes a blanket consent never covers); narrow connections list only callable tools by @jaspertey in #431
- MCP switch-over fixes: just-created workspace admission, Pennant array store kept across tenant switches, kit:status flags an undefined connection ability by @jaspertey in #433
- Read Blade, Livewire and Filament apps in the adoption inventory by @jaspertey in #435
- Semantic status text tokens: FormActions' Saved meets AA by @jaspertey in #441
- Guard useAppearance's localStorage access (blank page when storage is blocked) by @jaspertey in #444
- Scroll the Workbench's phone footer and inspector tabs; FormStatus on ResetPassword by @jaspertey in #446
- Release v0.7.0 by @jaspertey in #452
Full Changelog: v0.6.13...v0.7.0