Skip to content

17 October 2026

Three entries today:

  • The authorization server and the public MCP endpoint, from the first installation to switch them on
  • Migrations that converge with an older schema: order, names, a key rebuilt every run, duplicate tokens
  • schema:diff: an installation's tables against the framework's shape, and --apply

The authorization server and the public MCP endpoint, from the first installation to switch them on

FW-153.

Fixed

  • The MCP endpoint accepted the API's own sign-in JWT. The API's middleware identifies a caller from any token it signed, the sign-in token (tokentype auth) included, and the endpoint took that as a credential. A bearer now has to be an access token, issued by the authorization server or mcp:token; anything else gets the discovery 401. A call with no bearer (a same-origin session) is unaffected.
  • mcp:token signed with the console application's key and printed sURL/mcp. The endpoint is an API route, verified with the API's key under /<api>/<version>/mcp, so the token answered 401.
    • It now signs with the API's key and prints that address. --url is the API's base.
    • It adds a jti, so two tokens minted in one second no longer collide.
    • It refuses, saying to widen the column, when usertokens.token is too short for the encrypted token. An older varchar(255) cut it without a word.
  • The authorization server no longer makes its signing keys mid-request. A pair made that way belonged to the web server's user, was never backed up, and a new container made a new one.
    • Missing keys now fail with the command to run, in the oauth log and in the signing_keys health check.
    • A key the process cannot read says whose it is.
    • oauth:keys also makes encryption.key once, takes --owner=<user>, and warns when run as root without it.
  • Discovery named the authorization server two ways. issuer was https://host/ and authorization_servers was ["https://host"]. A strict client compares them and refuses the pair. Both now come from Discovery::issuer(), with the trailing slash that relying parties already compare iss with.
  • ApplicationService::create() and update() failed on an older applications table.
  • They wrote broadcast_secret, which that table lacks.
  • They wrote NULL into NOT NULL columns with no default.
  • They now write only the columns that exist, with '' or 0 where NULL is refused.

Changed

  • /.well-known/health answers status and 200/503 publicly. The components map, which names what is failing, appears only with 'health' => ['public_details' => true].
  • The consent screen is translated. Its words, the scope categories and every scope description go through t(), the MCP scopes' included.

Added

  • SchemaBuilder::columnLength($table, $column): the most characters a column holds, 0 for none. ApplicationService::callbackCeiling() uses it.

Documentation

  • The MCP, AuthServer Integration, Health, Console and Schema Builder guides describe the above.

Migrations that converge with an older schema: order, names, a key rebuilt every run, duplicate tokens

FW-154, the bugs. A fresh installation and one that had tables before the framework now end with the same indexes and keys.

Fixed

  • add_missing_indexes_to_existing_tables ran before the tables it indexes. At the default priority it ran before the auth feature made tokenactions and usertokens, did nothing and was recorded as run. So a fresh installation lacked the tokenactions indexes that one migrated table by table had.
    • It now runs at priority 200.
    • add_missing_indexes_after_their_tables runs it again for installations it already passed over.
    • FrameworkMigrationOrderTest holds the order for every migration that adds to other migrations' tables.
  • Indexes and foreign keys were looked for by name only. On a table that held them as tokenid or usertokens_ibfk_1, a second one was added. add_missing_indexes_to_existing_tables and add_missing_foreign_keys_to_existing_tables now look by columns as well.
  • cascade_usertokens_application rebuilt usertokens.
    • It dropped and re-created the key on every run, and on MySQL that copies the whole table.
    • It also added the key where the key-adding migration had skipped it for orphans or a type mismatch, and then failed for the same reason.
    • It now changes only a key that exists and does not cascade yet, in place.
  • add_token_lookup_to_usertokens failed on a token stored twice. It now adds a plain index where the digests repeat, and logs how many there are and how to resolve them.
    • Its encrypting backfill read usertokens without the prefix.
    • Whether it encrypts is now the encrypt_tokens setting (below), not the mere presence of an APP_KEY.

Added

  • 'auth' => ['encrypt_tokens' => false] keeps token values unencrypted even with an APP_KEY, for an application with lookups of its own on usertokens.token. Token::encryptsAtRest() says which way it is.
  • SchemaBuilder::indexColumns(), hasIndexOn() and hasForeignKeyOn() find an index or a key by what it covers.

Documentation

  • The Security Guide gives the order for moving an application's own token lookups, with the setting. The Migration Guide explains when a migration has to run late. The Schema Builder Guide covers the new methods.

schema:diff: an installation's tables against the framework's shape, and --apply

FW-154, the comparison and the convergence.

Added

  • schema:diff compares the database with the shape the framework's migrations would make.
    • The reference is built, not stored: the migrations run into an empty database on the same server with the same prefix, so the engines match. --reference names one for a user that may not create databases.
    • Columns are compared by type (without integer display widths), nullability, default and charset.
    • Indexes are compared by the columns they cover and whether they are unique.
    • Foreign keys are compared by column, target and delete rule.
    • Every difference has a key, and globs over the keys in schema.allow mark the intentional ones. It exits 1 while differences remain.
  • schema:diff --apply adds the missing columns and indexes, each exactly as the reference declares it. It changes and removes nothing.
    • This is the convergence: the CREATE migrations that return when their table exists stay as they are.
    • A latin1 or utf8mb3 column where the framework's is utf8mb4 is a charset difference.
  • Database\SchemaDiff: describe(), compare() and apply(), for a test of an application's own.

Documentation

  • The Migration Guide has a section on schema:diff. The Console Guide lists it.