Skip to content

feat(mcp): discover tool providers by registration event, not by scanning (#2077)#2083

Merged
rubenvdlinde merged 3 commits into
developmentfrom
feat/mcp-registration-event
Jul 24, 2026
Merged

feat(mcp): discover tool providers by registration event, not by scanning (#2077)#2083
rubenvdlinde merged 3 commits into
developmentfrom
feat/mcp-registration-event

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Proposal: or-mcp-registration-event

Summary

Discover MCP tool providers the way Nextcloud discovers everything else — a
typed registration event — and retire the bespoke scan.

Why

Two mechanisms in one app did the same job, and the older one was the more
complex.

MCP discovery probes every installed app's info.xml, builds candidate
container aliases (OCA\OpenRegister\Mcp\IMcpToolProvider::<appId>), resolves
each through the container catching autoloader misses, and caches the
resolution map with two invalidation mechanisms — an app-list hash, plus a
clamped TTL because an app upgrade can add a provider without changing the app
list. Roughly 86 lines of probing plus the cache, to discover nine providers.

That complexity exists because it scans for something apps never announce.
Apps do announce a listener.

Nextcloud has two idioms, and the split is the argument

  • Core owns a registry → IRegistrationContext::registerXProvider(class).
    Thirty-odd of these exist. Only core can add them, because only core can add
    methods to that interface.
  • An app owns a registry → a typed registration event. workflowengine
    does exactly this for operations, checks and entities.

OpenRegister is an app, so the event is the idiomatic choice for anything it
owns. That is already why the flow node registry uses one (#2074). Core ships
no MCP at all, so there is nothing upstream to conform to beyond the idiom.

What Changes

  • RegisterMcpToolProvidersEvent, mirroring RegisterFlowNodesEvent.
  • collectAnnouncedMcpProviders() runs FIRST; the alias scan still runs after
    it for one release so the fleet is never broken mid-migration.
  • A provider whose appId is already present is skipped, so a migrated app is
    never collected twice.
  • Discovery failure is logged, never fatal: an app with a broken listener costs
    its own tools, not everyone else's.

Migration

Five apps outside OpenRegister register by alias today: hermiq, openbuild,
opencatalogi, decidesk, and nextcloud-app-template. Each needs a listener.
The template goes first — it is what new apps copy.

The alias scan and its cache are removed in a follow-up, once the fleet has
migrated. Removing them in the same change would break every app that has not.

Out of scope

#[McpTool] attribute scanning and IMcpScannableServices solve a different
problem (which classes to reflect over) and are untouched.


Safe to merge before the apps migrate

Both paths run: announced first, alias scan after, with appId de-duplication between them. Nothing changes for an app until it adds its listener.

🤖 Generated with Claude Code

…ning

Two mechanisms in this app did the same job and the older one was the more
complex.

MCP discovery probes every installed app's info.xml, builds candidate container
aliases, resolves each through the container catching autoloader misses, and
caches the resolution map with two invalidation mechanisms — an app-list hash,
plus a clamped TTL because an app upgrade can add a provider without changing
the app list. Roughly 86 lines of probing plus a cache, to find nine providers.

All of it exists because it scans for something apps never announce. Apps do
announce a listener.

Nextcloud has two idioms and the split is the argument: when CORE owns a
registry it adds a `registerXProvider()` to IRegistrationContext — thirty-odd
of those, and only core can write them. When an APP owns a registry it
dispatches a typed event, which is what workflowengine does for operations,
checks and entities. OpenRegister is an app. That is already why the flow node
registry uses an event (#2074), and core ships no MCP of its own, so there is
nothing else upstream to conform to.

The announced path is collected FIRST; the alias scan still runs after it for
one release so the fleet is never broken mid-migration, and a provider whose
appId is already present is skipped so a migrated app is never collected twice.

Collection failure is logged, never fatal: an app with a broken listener should
cost its own tools, not remove MCP from the instance.

The scan and its cache come out in a follow-up, once the five apps that still
register by alias have migrated. Removing them here would break every one of
them.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ b27bbcd

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 174/174
npm ✅ 555/555
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-07-24 16:32 UTC

Download the full PDF report from the workflow artifacts.

…thods

Inserting collectAnnouncedMcpProviders() ahead of collectPerAppMcpProviders()
left the alias-scan's docblock sitting above the NEW method and the old one
with none — phpcs caught both. Each method now carries its own again.

0 errors across 1052 files; 88 flow tests green.
@rubenvdlinde
rubenvdlinde merged commit e2f88e9 into development Jul 24, 2026
1 of 2 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/mcp-registration-event branch July 24, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant