Metered licensing, a shared tool-error contract, and get_license_status
What changed
Metered licensing
McpConfiggainedMeteredPublicKey/MeteredPrivateKey(andSetMeteredKey(pub, priv)),
also read fromGROUPDOCS_METERED_PUBLIC_KEYandGROUPDOCS_METERED_PRIVATE_KEY.LicenseManagerresolution order is now metered keys, then license file, then evaluation,
applied once per process. Explicit config wins over the environment.- New
LicenseMode { Evaluation, Licensed, Metered }onILicenseManager, alongside the existing
IsLicensed(true for bothLicensedandMetered— metered is a licensed state). ILicenseManager.GetConsumption()returns aMeteredConsumptionreading, ornulloutside
metered mode.- Two new warnings that previously had no equivalent: only one of the two keys set (the
configuration is ignored — this must never look like success), and both metered keys and a
license file set (metered wins, the file is ignored). - The private key is never logged. The public key is masked to a four-character prefix.
Shared tool-error contract
- New
ToolErrorFilter, registered automatically byAddGroupDocsMcp()as an MCP
CallToolFilter. It converts tool exceptions into the errors-as-text contract with
isError: true. FileResolvernow acceptsfileNameon its own as an alias forfilePath.- The "file not found" listing is no longer silently capped: it honours the new
McpConfig.MaxListedFiles(default 50) and appends...and N more. FileInputfield descriptions now match the implemented contract.
New tool
get_license_status— shipped in Core and registered automatically, so it exists on every
server without per-product code. Returnsmode,licensed,source,consumption, and the
server/enginenames and versions.
Why
An external audit of all 12 published MCP servers (2026-08-16; 46 defects reported, all 46
independently reproduced, zero false positives on re-validation) found three defects living here
and therefore present on every product:
fileNamecrashed every tool. The tool descriptions tell callers to "just pass the filename
the user provided" and the schema permitted it, but that form threwArgumentExceptionfrom
FileResolver.ResolveAsync. Because tools resolve their file outside theirtryblock, the
exception escaped and the client saw onlyAn error occurred invoking '<tool>'.- Missing files returned an opaque error. The excellent recovery message — every available
file with sizes — was built and then discarded to stderr, contradicting the promise in every
tool description. It was also cut off at 20 entries with no marker, which reads as "the file
really is not there". isErrorwas inverted. Resolver crashes set it; genuine engine failures did not, because
tools returnstringand the SDK wraps a returned string as success. The flag meant "we
crashed", not "the operation failed", so a client could not detect failure programmatically and
had to string-match on error prose.
A missing required parameter was equally opaque — worst on Total, where 38 similarly-named
tools make wrong parameter names the most likely client mistake.
All of these are now handled in one filter, so every failure carries both a readable message and
isError: true.
Metered licensing is a natural fit for MCP: agent workloads are bursty and unpredictable, which is
exactly the profile pay-per-use licensing exists for, and one key pair works regardless of which
platform's server a customer runs.
Migration / impact
Breaking for implementors of LicenseManager. Two new abstract members must be implemented:
protected override void SetMeteredKeyCore(string publicKey, string privateKey)
=> new GroupDocs.<Product>.Metered().SetMeteredKey(publicKey, privateKey);
protected override MeteredConsumption ReadConsumptionCore()
=> new() { Quantity = GroupDocs.<Product>.Metered.GetConsumptionQuantity(),
Credit = GroupDocs.<Product>.Metered.GetConsumptionCredit() };They are abstract rather than virtual on purpose: a no-op default would let a server accept metered
keys and silently run in evaluation mode while the customer believed they were billed per use —
the same silent-failure class this release is fixing.
No Program.cs change is required. The filter and get_license_status are contributed from
AddGroupDocsMcp() via Configure<McpServerOptions>, which applies even though it runs before
AddMcpServer(). Verified end to end against a live stdio server.
Products may now delete their per-tool try/catch and ToolError.cs — the filter formats
engine failures, including the inner-exception chain, and sets isError. This is optional; leaving
them in place is harmless, as the tool's own catch simply wins.
Core now references ModelContextProtocol.Core (1.1.0), which every product already pulls
transitively via ModelContextProtocol.
Behaviour change worth noting for tests: assertions written as
IsError || text.Contains("not found") passed on the old defect. They will still pass, but they no
longer prove anything — assert the promised Available files: text instead.
Full Changelog: 26.4.1...26.9.0