Skip to content

26.9.0

Latest

Choose a tag to compare

@github-actions github-actions released this 02 Sep 22:52

Metered licensing, a shared tool-error contract, and get_license_status

What changed

Metered licensing

  • McpConfig gained MeteredPublicKey / MeteredPrivateKey (and SetMeteredKey(pub, priv)),
    also read from GROUPDOCS_METERED_PUBLIC_KEY and GROUPDOCS_METERED_PRIVATE_KEY.
  • LicenseManager resolution 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 } on ILicenseManager, alongside the existing
    IsLicensed (true for both Licensed and Metered — metered is a licensed state).
  • ILicenseManager.GetConsumption() returns a MeteredConsumption reading, or null outside
    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 by AddGroupDocsMcp() as an MCP
    CallToolFilter. It converts tool exceptions into the errors-as-text contract with
    isError: true
    .
  • FileResolver now accepts fileName on its own as an alias for filePath.
  • The "file not found" listing is no longer silently capped: it honours the new
    McpConfig.MaxListedFiles (default 50) and appends ...and N more.
  • FileInput field 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. Returns mode, licensed, source, consumption, and the
    server / engine names 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:

  • fileName crashed every tool. The tool descriptions tell callers to "just pass the filename
    the user provided" and the schema permitted it, but that form threw ArgumentException from
    FileResolver.ResolveAsync. Because tools resolve their file outside their try block, the
    exception escaped and the client saw only An 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".
  • isError was inverted. Resolver crashes set it; genuine engine failures did not, because
    tools return string and 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