Changelog for modxmcp.

1.0.0-beta1
===========
First release.

- MCP revision 2026-07-28 over Streamable HTTP. Stateless, no SSE.
- Endpoint is a MODX resource calling the modxmcp snippet, so the package
  ships no web-accessible PHP and works on installs that deny PHP under
  assets/.
- Token authentication: hashed at rest, per-token scopes, optional expiry
  and IP allowlist, last-used tracking.
- Audit log covering rejected calls as well as successful ones.
- Manager page for token management and audit review.
- Resource and element tools, all writes through MODX processors so
  Manager-side behaviour fires: SeoSuite registration, Collections rules,
  cache invalidation and lifecycle events.
- Schema discovery across every installed extra, handling both package
  layouts and both metadata formats, with field descriptions taken from
  each extra's own lexicon.
- Generic object access, opt-in per class for both reads and writes, with
  a hard block on users, sessions, access-control rules and modxmcp's own
  tables that no setting can override.
- Adapters for SeoSuite (redirects), MIGX (item structure) and Collections
  (container listing), registered only where the extra is installed.
- OnMCPRegisterTools event so third-party extras can contribute tools. A
  failing plugin degrades to its tools being absent, never to a dead
  endpoint.
- Zero third-party PHP dependencies.

1.0.0-beta2
===========
Security fixes found by review before any public release.

- SQL injection in generic object filters. Filter keys accept an optional
  comparison operator after a colon, and xPDO interpolates that operator
  into SQL rather than binding it. Only the field half was validated, so a
  key such as "id:) OR 1=1 -- " produced a working injection. Operators are
  now checked against a fixed allowlist and the key is rebuilt from the
  validated parts. Values were always bound and were never the risk.
- ClassGuard matched class names case-sensitively while PHP resolves them
  case-insensitively, so a differently-cased spelling of a hard-blocked
  class did not match the block list. Both the block list and the
  allowlists are now case-insensitive.
- modxmcp now refuses to act as a MODX user that belongs to no user group.
  MODX treats "no access policy" as unrestricted, so such a user holds
  every permission; binding a token to an ordinary-looking account with no
  groups produced the most privileged token possible rather than the least.

1.0.0-beta3
===========
- The endpoint now ships enabled. Access is granted by tokens, not by that
  switch: a fresh install has none, so every request is rejected until an
  administrator creates one. Requiring a System Settings edit first was
  friction for no security benefit. The setting remains as a kill switch.
- All modxmcp settings are now editable on the extra's own page, under a
  Settings tab, alongside the endpoint URL and live token and audit counts.
  Nothing about administering this extra requires leaving its page.
- Fixed the manager page failing to load at all: the controller stub
  extended a class before the autoloader was registered, and home.tpl used
  MODX tag syntax in a file parsed by Smarty.
- Removed the Components menu icon, which pushed the description onto its
  own line with a visible gap. No other extra sets that field.

1.0.0-beta4
===========
- Dual-era protocol support. The endpoint now serves both 2026-07-28 and the
  initialization-based era (2025-11-25, 2025-06-18, 2025-03-26), choosing per
  request by how the client opens: per-request _meta selects the modern
  revision, an initialize handshake selects the older one. Every shipping MCP
  client still speaks the older era, and 2026-07-28 does not publish until
  2026-07-28, so a modern-only server could not be connected to by anything.
  No sessions are assigned in either era; the token identifies every request.
- resource_create and resource_update now re-read the saved row instead of
  returning what the processor echoes. Resource/Create returns only an id,
  and Resource/Update strips pagetitle, longtitle, content, introtext,
  description and menutitle.

1.0.0-beta5
===========
Correctness release. Every fix below is a case where a tool reported success
while the site did nothing, or did something other than what was asked.

Behaviour changes
-----------------

- Resources are created as MODX\Revolution\modDocument, not the abstract
  MODX\Revolution\modResource.

  Resource/Create already defaults to modDocument; modxmcp was overriding it on
  every single write. The Manager never creates a modResource, and extras that
  key on class_key (Collections, Articles, type-specific rendering) do not
  recognise one.

  Resources created by earlier versions keep the wrong type. They are not
  rewritten automatically, because that would be a write you did not ask for on
  a value some sites may have set deliberately. resource_get and
  resource_update now flag them, and resource_update can repair them:

      modxmcp_resource_update {"id": 42, "class_key": "MODX\Revolution\modDocument"}

- Writing a template variable that is not attached to the resource's template
  is now an error. It previously returned success and wrote nothing.

  The Resource processors only iterate TVs that the resource's template
  declares, so a tv{id} property for anything else was accepted, submitted and
  discarded inside the loop. Validation now runs before the processor, so a
  rejection leaves nothing half-written, and the error names what the template
  does declare. Callers whose TV writes were silently doing nothing will start
  seeing failures; those writes were never landing.

- element_save rejects type-scoped arguments sent for the wrong element type,
  e.g. events on a chunk. Genuinely unknown keys are still ignored, as before,
  so no existing caller can break on an argument this tool never claimed to
  read.

Fixes
-----

- Generic writes to resource and element SUBCLASSES were not blocked.

  ClassGuard matched class names with anchored patterns and no ancestry check,
  so MODX\Revolution\modResource was blocked while MODX\Revolution\modDocument,
  modWebLink, modSymLink, modStaticResource and every extra-provided container
  class were not. Since modDocument is what nearly every real page is, a site
  with modxmcp.write_class_allowlist set to * was exposed to generic
  xPDO::save() writes against its content, bypassing the processors this extra
  exists to route through. The guard now tests ancestry.

- resource_update never wrote a template variable at all. Long-standing, and
  found by the new tvs echo on the first live run.

  Resource/Update::saveTemplateVariables() opens with getProperty('tvs') and
  skips the whole block when it is empty, so the tv{id} properties it is given
  are ignored without it. The Manager form posts tvs=1 and nothing documents
  that the flag is load-bearing. Resource/Create has no such gate, which is why
  creating with TVs always worked and updating with them silently never did.

- Changing a resource away from modWebLink or modSymLink is now declined.
  MODX renders the old type while saving, and those types end their render by
  issuing their own redirect and calling exit, so the change was applied and
  then the HTTP response was destroyed before the caller saw it. A write whose
  outcome cannot be observed is the failure this release is about, so the tool
  refuses and names the alternative. Changing TO those types is unaffected, as
  is the modResource repair path.

- Changing template and template variables in one resource_update call resolved
  the TVs against the OLD template, resubmitting values MODX then discarded.
  They are now resolved against the template the resource will have, and the
  response warns which TVs fell out of, and came into, effect.

- MIGX values are still JSON-encoded, but tag and autotag TVs are now
  comma-joined rather than JSON-encoded. The processor calls explode(',') on
  them unconditionally, so an array was a TypeError and JSON was the wrong
  content.

Additions
---------

- pub_date, unpub_date and publishedon on resource_create and resource_update, so
  a post can be scheduled. Previously nothing in the tool surface could set them
  and every publish was immediate.

  Dates are normalised before they reach MODX, which parses these fields with a
  bare strtotime() and validates nothing. A UNIX timestamp sent as a string is
  the trap: strtotime() returns false for it, the processor reads false as a date
  in the past, and the resource publishes immediately instead of being scheduled.
  Numeric strings are now refused with the correctly formatted date in the error.

  publishedon is carried with the `published` value it needs, because MODX
  discards it when `published` is absent from the same call. publishedby is
  refused on update, where MODX overwrites it unconditionally.

  A future pub_date together with published=true is normalised to unpublished and
  explained in warnings; MODX resolves that contradiction the same way, silently.

  resource_create and resource_update also report pub_date and unpub_date, which
  is what makes "did the schedule take" answerable. Note these are int columns
  with a `timestamp` phptype, so xPDO renders them back as 'Y-m-d H:i:s'.

- The publish state is verified after the write. Without publish_document MODX
  silently restores published, publishedon, pub_date and unpub_date to their
  stored values and still reports success; the tool now compares the re-read row
  against what was asked and says so.

- class_key on resource_create and resource_update. Accepts short names
  (modWebLink) as well as fully-qualified ones, and validates against the
  resource types the site actually has, so Collections containers, weblinks,
  symlinks and static resources can now be created. An unresolvable value is
  refused rather than written, because MODX instantiates resources by class_key
  and a bad one produces a row only SQL can remove.

- resource_create and resource_update return a tvs field echoing the values
  actually stored, for the names you passed. Absent when you passed no TVs, so
  responses are otherwise unchanged.

- element_save takes events (plugins) and templates (template variables), so the
  bindings that decide whether an element does anything at all can finally be
  written. Both ride along on the same processor call as the save, so there is
  no window where the element exists unbound. Passing a list replaces the
  current bindings; omitting the argument leaves them alone. An unknown system
  event or template is refused before anything is written, because a binding to
  an invented event would silently never fire. element_save and element_get both
  echo the current bindings back.

- element_save still warns when a plugin ends up bound to no events, or a
  template variable attached to no template. Reaching that state is now a choice
  rather than the only option.

- element_get for a template lists the template variables attached to it. Those
  are exactly the TVs resource_create and resource_update can write on a
  resource using that template, and nothing else in the tool surface exposed the
  relationship.

- site_info reports advisories: conditions this site actually exhibits, with the
  evidence and a machine-checkable rule, rather than fixed prose keyed on an
  extra being installed.

  The old warnings fired whenever a namespace was present. Collections installed
  with no containers still produced the show_in_tree warning, which is noise on
  the first call of every session, and noise trains a reader to skim the block
  that also carries the real blockers. Probes now detect, and stay silent when
  there is nothing to say.

  Five ship: Collections children (only when containers exist, carrying their
  real ids so a caller can test its own pending parent against them), SeoSuite
  sitemap orphans (an audit, emitted even at zero so "checked and clean" is
  distinguishable from "not installed"), reserved TV names (derived from the
  resource column set rather than a hardcoded list, and stating explicitly that
  modxmcp's own TV reads and writes are unaffected), package updates, and cache
  invalidation. A probe that throws costs its own advisory and nothing else.

  warnings stays a string[] carrying every summary, so existing callers are
  unaffected. Third-party extras can contribute their own through the new
  OnMCPCollectAdvisories event.

- modxmcp.site_notes, a system setting surfaced by site_info. Free text for local
  convention nothing can detect: which parent new articles go under, whether
  publishing is live immediately. Capped at 8 KB, because it rides on the first
  call of every connection, and never parsed.

- The Collections predicate had grown in four places in three shapes: a stripos()
  on a parent's class_key, a stripos() on the resource's own, a class_key:LIKE
  query fragment, and prose. All four now come from one trait, which also holds
  the canonical wording of the rule.

- modxmcp_resource_duplicate. Copies a resource and then re-saves the copy
  through the normal update path.

  That second step is the point. Resource/Duplicate extends the plain Processor
  rather than CreateProcessor and fires only OnResourceDuplicate, so none of the
  Manager save path runs for a copy: SeoSuite never registers it and it is
  silently missing from sitemap.xml, Collections never applies its rules, and the
  cache is never cleared. A thin wrapper would have shipped exactly the bug this
  extra exists to prevent.

  Core's property names are inconsistent (name, duplicate_children,
  prefixDuplicate, published_mode); the tool presents one convention. MODX does
  not accept a parent when duplicating, so the copy always lands beside the
  original and the description says to move it afterwards. On an alias collision
  MODX empties the alias rather than making it unique, and with friendly URLs off
  does not check at all; both cases are now warned about.

- modxmcp_updates. Reports the extras installed on this site with their versions
  and whether MODX or any extra is behind, reading the same cache the Manager
  dashboard shows rather than contacting anyone. No outbound request, and it
  cannot disagree with what an administrator sees.

  There is no "update available" column to read: modTransportPackage stores
  version and provider and nothing about newer releases. MODX answers this in two
  places and both are used. The dashboard widget caches one combined entry under
  mgr/providers/updates/modx-core covering MODX itself and the outdated extras by
  name; the Extras grid separately caches a per-package count, and that is
  preferred where present.

  Every package reports one of three states: update_available, current, or
  unknown. Unknown is not folded into current. Both caches expire, so a site
  nobody has opened the dashboard on lately genuinely does not know, and saying
  otherwise would be an assertion with nothing behind it. The response says how
  many packages are unknown and what to do about it, and names
  auto_check_pkg_updates when that setting is what is stopping the checks.

  refresh=true asks the update service directly, using the same two processor
  calls, cache key and TTL as the dashboard widget, so a refresh here is
  indistinguishable from someone opening the dashboard. Off by default, since it
  is the only part that touches the network.

  Superseded rows are collapsed by default so each extra appears once at its
  newest version, as the Extras grid does; MODX keeps every version ever
  installed, which on a mature site is most of the rows.

  It deliberately cannot install, update or uninstall anything. Applying an
  update runs third-party code and changes database schema inside a web request,
  which belongs in the Manager with a human watching.

- modxmcp_search, over element bodies and resource content together. "Who calls
  [[$productCard]]?" is one question, and its answer is spread across template
  bodies, other chunks, snippets that build markup, and resource content where
  an editor pasted the tag by hand; two tools would mean a caller that asks half
  the question and believes it has the answer.

  mode=tag_reference expands an element name to the MODX tag forms that would
  call it ([[$name]], [[name]], [[!name]], [[*name]], [[+name]]), which is what
  you want before a rename. Every match is reported with its line number, not
  just the first, because a rename needs every call site. The needle is matched
  literally: regular expressions are not accepted, having no use here and being
  a denial-of-service primitive. Wildcards are escaped, so searching for "100%"
  no longer matches every row. Excerpting is multibyte-safe, which matters more
  than it sounds: slicing UTF-8 with substr() can split a codepoint, and
  json_encode() answers invalid UTF-8 by returning false for the entire payload,
  so one accented character could empty a whole response.

- resource_list takes template, class_key and hidemenu filters, and sort with
  dir. The sort column is validated against the resource field map, the same way
  object_list validates its own, because a column name reaches SQL as structure
  rather than as a bound value. The default order is unchanged: callers page
  through this tool, and silently reordering it would renumber every page.

- modxmcp_category_list and modxmcp_category_save. Categories were the one part
  of the element surface with no way in: element_save took a numeric category id
  and element_list returned one, while nothing mapped an id to a name, so "put
  this in the Blog category" had no path to an answer. element_save.category now
  accepts a name as well as an id. A name that does not exist is refused rather
  than created, because auto-creating on save is how a site ends up with "Blog",
  "blog" and "Blog " as three separate categories.

- element_save and category_save re-read the saved row instead of returning what
  the processor echoed, which resource_create and resource_update already did.
  Element cleanup() returns a hand-picked subset that omits static, static_file
  and source entirely, so those could never be confirmed from a response, and
  category_save had a '?? $properties' fallback that existed precisely because
  the echo was already known to be unreliable. Both now fail loudly if the
  processor reports success without returning an id.

- The alias-change warning names modxmcp_seo_redirect and the arguments to call
  it with, instead of only mentioning that SeoSuite is installed.

Internal
--------

- ResourceSupport::tvProperties() removed. Resolution lives in the new
  TemplateVarSupport trait, which resolves against the attached set. Removed
  rather than deprecated: a wrapper that skipped the attachment check would
  have kept the silent-drop path alive. Only reachable by a third-party tool
  that used the trait directly.

1.0.0-pl
========
First stable release. Everything below is in addition to the beta5 entry.

- modxmcp_site_info now reports a build fingerprint alongside the version:
  {"version": "1.0.0", "build": "<hash>", "files": 76}. The version alone
  cannot say which code is running. An rsync deploy changes the files without
  changing it, and a rebuilt transport package keeps its signature by
  definition, so two different builds of 1.0.0-pl were indistinguishable from
  anywhere. The fingerprint hashes every PHP file under src/ and is computed
  per request, never cached, because a cached value would survive an rsync and
  then assert the wrong build with total confidence.
- The build now refuses to run unless PKG_VERSION, Server::VERSION and the
  newest changelog heading agree. They had drifted for five releases:
  1.0.0-beta5 shipped reporting 0.4.0. _build/build.transport.php also prints
  the fingerprint of what it packaged.
- New dev/package-install.php installs by signature after removing the stale
  transport_packages row and the stale extracted directory, then prints the
  fingerprint of what was installed. Both removals are required: ScanLocal
  skips signatures it already knows, and modTransportPackage::getTransport()
  only unzips when core/packages/<signature>/ is absent. Reinstalling a rebuilt
  package therefore installed the previous build, silently, while continuing to
  report the expected version. That is what reverted a site to beta2 during
  this release.
- Removed dev/package-install-test.php, which installed whichever modxmcp
  package it happened to find first.

- element_save can now define an element, not just name and fill it. A template
  variable was always a plain text field because the tool forwarded `type` and
  nothing else; a MIGX, listbox or image TV could not be created or configured
  at all. Adds input_type, caption, elements, input_properties,
  output_properties, display and rank for template variables, default properties
  for snippets, plugins and templates, disabled for plugins, and locked for all.

  MODX's property names for these bear almost no relation to the columns:
  `elements` is submitted as `els`, and a TV's option blobs are not objects to
  the processor at all -- it scans every submitted property for an `inopt_` or
  `prop_` prefix and rebuilds the column from what it finds. The mapping lives
  in one table. Omitting the option blobs is safe: the Update processor only
  writes those columns when at least one prefixed key is present, so an
  unrelated save leaves a MIGX configuration alone.

  Default properties do not ride on the save. `propdata` is read by the Create
  processors and ignored by every Update processor, which would have made this a
  silent no-op on update, so it goes through the same separate processor the
  Manager uses.

  `display_params` is not offered: the processor docblocks advertise it and the
  xPDO map has no such column, so it never did anything.

- resource_get takes include_tv_state, reporting per template variable whether
  the value is stored on the resource or inherited from the TV default.
  getValue() falls back to default_text only on a strict null, so a TV
  deliberately blanked over a non-empty default is otherwise indistinguishable
  from one nobody has set. Off by default and emitted as a sibling key, so the
  tvs map that resource_update round-trips is unchanged.

- OnMCPCollectAdvisories is registered on install. It shipped in the code and
  the changelog in beta5 without a modEvent row, so no plugin could bind to it.

- The readme said this server implements revision 2026-07-28 only and rejects
  earlier clients. Dual-era support shipped in beta4; every currently-shipping
  MCP client speaks the older era, so the packaged documentation was telling the
  entire real-world client population not to bother. It also claimed every write
  goes through a processor, which is untrue of generic object access and SeoSuite
  redirects. Both corrected, and the readme now lists the tools.

- warnings is present only when there is something to say. Five tools emitted an
  empty array and four omitted the key, splitting single tool families:
  element_save guarded while element_delete did not. site_info is deliberately
  unchanged, because there the field is the payload rather than a diagnostic
  attached to a write. The two generic-object tools keep their permanent
  xPDO-bypass disclaimer.

- Test coverage for the five tools neither suite touched: object_list (which
  carries the beta2 SQL-injection fix and had no regression test), object_save,
  schema_list, schema_describe and updates, plus the snippet element type.

- New modxmcp_file_upload tool: one file per call, base64-encoded, written
  through the Browser/File/Upload processor so the media source access
  policy, the upload_files and upload_maxsize settings and the
  OnFileManagerBeforeUpload / OnFileManagerUpload events all apply exactly
  as they would for a Manager upload. The processor reads $_FILES, which is
  swapped in around the one call and restored in a finally block; Flysystem
  underneath reads the staged temp file with file_get_contents(), not
  move_uploaded_file(), which is what makes this possible without touching
  MODX.
- Ships disabled. Four new settings on the Settings tab gate it, and the
  directory list is empty on install: modxmcp.upload_path_allowlist (empty
  means no uploads), modxmcp.upload_extension_allowlist (default images and
  pdf), modxmcp.upload_max_bytes (default 10 MB, decoded) and
  modxmcp.upload_source_allowlist (default the filesystem source). Every
  refusal names the setting an administrator must edit.
- New write:media token scope, offered as a checkbox on token creation.
  Existing tokens do not gain the scope by upgrading.
- Server-executable extensions (php and variants, phtml, phar, shtml, cgi,
  asp, jsp, htaccess, htpasswd, ini for .user.ini) are blocked in EVERY
  dot-segment of the filename, not just the last one, because an Apache
  AddHandler matches inner segments and "shell.php.jpg" is the classic way
  an upload gate gets outrun. No setting can enable them. Content containing
  a PHP open tag is refused whatever the name says.
- Uploads are verified by reading the file back through the media source
  after the processor reports success, because uploadObjectsToContainer can
  skip a file without recording an error, and a success that wrote nothing
  is this extra's least favourite failure mode. Overwrites are opt-in and go
  through Browser/File/Remove first, so removal events fire too.
- The audit log now redacts content_base64 before storing arguments, so an
  upload with log_arguments enabled records who wrote which path, never a
  payload dump.

1.0.1-pl
========
Manager fixes.

- The settings Save button was rendered in the panel footer, which the
  autoHeight tab clips out of frame. Save now appears twice: in a top
  toolbar like the other tabs, and in-flow after the last fieldset.
- New Clear log button on the audit grid, behind a confirmation. It
  deletes every row regardless of the grid's current filters, and writes
  who cleared the log and how many rows to the MODX system log.

1.0.2-pl
========
Protocol conformance under revision 2026-07-28.

- Results served under 2026-07-28 carry `resultType`. The revision requires
  it on every result and a client is entitled to refuse one without it:
  absence means "complete" only for earlier revisions. Claude Code rejected
  every tools/list, so no tools registered at all on any deployed site
  while the endpoint otherwise looked healthy. tools/call was wrong the
  same way and would have failed the moment a tool was invoked.
- Complete results of server/discover, tools/list, prompts/list,
  resources/list and resources/templates/list carry the caching hints
  `ttlMs` and `cacheScope`, which that revision also requires. Five
  minutes, and `private` rather than `public`: the tool list is identical
  for every token today, but the specification permits filtering it by
  granted scopes, and `public` would license a shared proxy to serve one
  caller's list to another the day it is.
- Both are stamped by the protocol revision rather than by each handler,
  so a result cannot reach the wire without them. Results served to
  initialization-based clients are untouched: neither field exists before
  2026-07-28.

Nothing changed in the legacy path, in tool behaviour, or in any site's
data. The upgrade rewrites files only; its resolvers are idempotent, and
existing system settings, tokens, the audit log and the endpoint resource
are all preserved.
1.1.0-pl
========
Answers a full external audit of the tool surface against 1.0.2 — issues #1
through #4 and #7, plus two pull requests — and one thing that audit did not
find.

REQUIRES PHP 8.2. Not a new requirement so much as an honest one: the support
traits declare constants, which PHP only permits in a trait from 8.2, and
ExcerptSupport and ObjectSupport have done so since 1.0.0. On PHP 8.1 those
files fatal at include time, taking modxmcp_search and the three generic
object tools with them, while the package advertised >=8.1. If you are on 8.1
those four tools have never worked; the package now says so before installing
rather than failing silently afterwards.

Discovery
- The MODX core is discovered. The `core` namespace was registered like any
  other but its model map sits at core/src/Revolution/metadata.mysql.php, one
  level shallower than an extra's, so no glob matched it and the whole core was
  invisible. ClassGuard's hard-block list names almost nothing but core
  classes, so it had never once run.
- The core's deeper maps are discovered too: media sources under
  src/Revolution/Sources/ and the transport classes under
  src/Revolution/Transport/. glob() does not let * cross a separator, so the
  pattern that found the core stopped one directory short of both.
- A hard-blocked class now says so. It used to answer "Unknown class ... use
  modxmcp_schema_list", which was wrong twice over, since the class existed and
  schema_list would never have named it.

New tools
- modxmcp_media_source_list — the media sources this site defines, with the id
  modxmcp_file_upload expects, where each one is rooted, and whether the upload
  source allowlist covers it. Credentials are never returned. Gated on MODX's
  own source_view permission, and remote sources are not contacted unless
  asked, because initialising an S3 source checks the bucket over the network.
- modxmcp_category_delete — removes a category, refusing one that still holds
  elements or sub-categories and naming what is in it. Stricter than MODX,
  which deletes the category regardless, resets everything inside it to
  uncategorised, and cascades through sub-categories without saying so.

Data loss fixed
- modxmcp_element_get returns the fields modxmcp_element_save replaces
  wholesale: a snippet, plugin or template's default properties, and a template
  variable's caption, display, option list, rank and input and output
  configuration. element_save's description told callers to "send every option
  you want to keep" and nothing in the surface could say what those were, so
  adding one property destroyed the others with the loss invisible short of
  reading the database.
- modxmcp_element_delete echoes the same fields, and a plugin's event bindings,
  gathered before the processor removes them. Element removal is permanent and
  that echo is the recovery path; a plugin rebuilt from the old one came back
  bound to nothing and never ran again.

Refusals and warnings
- A refusal raised by a tool is reported as a result carrying isError, not as a
  JSON-RPC protocol error. Clients are entitled to render a 4xx as a transport
  failure, so the most useful messages in the extra — the ones that name the
  problem, list the valid values and say "Nothing was written" — were the ones
  arriving mangled, while "the details are in the error log" travelled the
  reliable path. Routing and authorisation stay protocol errors: both are
  raised before a tool runs.
- The audit log records a framed failure as a failure. It could not before, and
  an unexpected exception inside a tool had been recorded as a SUCCESS since
  the registry was written, because those have always been framed rather than
  rethrown.
- modxmcp_element_delete warns when something still calls the element it
  removed, naming the resources and elements with their URIs and published
  state. modxmcp_search has answered "who calls this" since 1.0.0 and the
  delete path never asked it, so a chunk called by two published pages was
  removed without a word and those pages rendered without it.
- modxmcp_element_save warns when a template variable's input_type is one
  nothing can render. Its own description warned that "radio is not a type",
  and then accepted radio silently. Extras register their own types, so this is
  a warning and not a refusal, and it recognises a type any template variable
  on the site already uses.
- Deleting a template that resources still use is refused up front, naming how
  many. MODX rejects it anyway; the tool's description had claimed the
  resources would be left without a template, and the warning that counted them
  was unreachable code appended to a result built after the processor throws.

Smaller
- tools/list is filtered by the calling token's scopes. A read-only token was
  offered every write tool and would announce a publish, attempt it and spend a
  round trip on the refusal; it now sees thirteen tools, all of which it can
  call.
- modxmcp_schema_list no longer suppresses its explanatory note in the one case
  that needs it, where every discovered class is hard blocked and the response
  was an unexplained {"shown":0,"readable":0}.
- modxmcp_resource_duplicate echoes the template variable values the copy
  inherited, which its description had always promised.
- modxmcp_cache_refresh clears modxmcp's own discovery and advisory caches on a
  full clear. It never had, and their 300-second TTL was the only thing that
  expired a stale class list, so installing an extra and then calling the tool
  whose whole job is clearing caches still showed the old site.
- Media sources and package providers are permanently blocked from generic
  access. Both keep credentials inside a serialised properties column, where
  field-name masking cannot see them.
- Documented that modxmcp.upload_path_allowlist is relative to the media
  source, not to assets/. The Filesystem source MODX ships has no configured
  base path and therefore resolves to the webroot, so one allowlist entry means
  different directories on different sources.

Nothing in this release changes any site's data, and the upgrade rewrites files
only. Existing system settings, tokens, the audit log and the endpoint resource
are all preserved.
