Skip to content

v0.5.85

Choose a tag to compare

@jaspertey jaspertey released this 30 Oct 02:51
8db8ec7

Upgrading

  • Run kit:update plan|apply for mcp (re-pinned sha256:7e095959…f01b → sha256:29d14952…750e): Mcp/Authorize/Page.vue shows the redirect host and the unverified-app note below, and the migration 0001_01_02_000603_create_kit_mcp_registrations_table is new. Then php artisan migrate.
  • The mcp feature now serves its own /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource documents (root and path-suffixed) and no longer calls Mcp::oauthRoutes(). A host that re-declared those routes over laravel/mcp's (to advertise its scopes) can drop them: scopes_supported is the kit's token scopes, which include the host's. A host that re-declared POST /oauth/register over laravel/mcp's controller can drop that too and set kit.mcp.registration to true; its existing clients keep working. token_endpoint_auth_methods_supported is now ["none"] only.
  • A host's unregistered-redirect_uri test that expected Passport's 401 invalid_client JSON for a browser request now gets the kit's 400 page; JSON requests keep the 401. The laravel/mcp requirement is ^1.0.2.
  • A client that registered http://localhost/... (or http://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 for https redirects.
  • New migration for mcp, 0001_01_02_000603_create_kit_mcp_registrations_table (the kit_mcp_registrations table: which OAuth clients came from POST /oauth/register). Run kit:update plan|apply for mcp, then php artisan migrate. Until it is migrated, registration answers 500 server_error and kit:mcp:clients refuses. 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 --revoke and --prune-unused can touch it; the kit never guesses.

Added

  • MCP client registration and discovery, the kit's own (backported from main; 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: https with a host, http to a loopback host on any port (Claude Code's per-session port, RFC 8252), or a private-use scheme the host lists in kit.mcp.redirect_schemes (empty by default); never a wildcard, fragment, userinfo, plain http elsewhere, or javascript, data, blob, file. A per-IP rate limit (kit.mcp.registration_per_minute, 10) and an optional kit.mcp.registration_policy (a RegistrationPolicy class refusing with a reason, 403 access_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_code and refresh_token, S256, public clients, the kit's token scopes, and registration_endpoint only when registration is on; Mcp::oauthRoutes() is no longer called. The 401 on /mcp adds scope="mcp:use" to laravel/mcp's challenge. Passport's client bridge matches a loopback localhost redirect without its port, as league already does for 127.0.0.1. kit:mcp:clients lists registered clients, --revoke=<id> revokes one with its tokens, --prune-unused=<days> revokes those with no token in that time (--dry-run to look first). composer test:mcp-registration covers 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. AuthorizeViewModel gains redirect_host and registered; the Mcp/Authorize/Page.vue stub shows them.
  • PKCE S256 required at authorize. Every authorization request must carry a code_challenge (43 to 128 unreserved characters) with code_challenge_method=S256; a missing challenge or the plain method 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 bare invalid_client JSON; JSON callers and errors sent back to a verified redirect URI are unchanged. The kit's kit::oauth-error view by default; a host sets kit.mcp.oauth_error_view to its own view, or OAuthErrorPage::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.

What's Changed

  • [0.5.x] MCP dynamic client registration, kit-owned discovery, PKCE S256, brandable OAuth error page (backport of #406) by @jaspertey in #419

Full Changelog: v0.5.84...v0.5.85