Summary
Evaluate whether the managed Rust-backed plugins in cpex-plugins should keep per-plugin Python gateway shims, or whether the hook boundary should move lower so the gateway can execute them through a Rust-native contract.
The current model works, but it keeps a Python package and Python import surface on every managed Rust plugin even when the actual plugin behavior is already implemented in Rust.
This issue is to make that tradeoff explicit and decide whether we should:
- keep the current Python package ABI and just reduce boilerplate,
- introduce a shared Rust-native hook bridge while still publishing Python packages, or
- move the hook/runtime boundary directly into
IBM/mcp-context-forge and potentially relocate some of this responsibility there.
Current state
Today this repo is explicitly organized around Rust plugins published as Python packages:
README.md and DEVELOPING.md define the managed layout as plugins/rust/python-package/<slug>/
- each plugin publishes a Python entry point under
[project.entry-points."cpex.plugins"]
- each plugin manifest declares
kind in Python module.object form
tools/plugin_catalog.py validates that Python package/module/object contract
- ADR-048 in
IBM/mcp-context-forge explicitly chose PyO3-backed packages because the gateway still imports plugins as normal Python modules
That means the Python layer is not just incidental packaging today; it is part of the current plugin ABI.
At the same time, the amount of Python-specific logic varies a lot across plugins:
Mostly thin gateway shims today
pii_filter
secrets_detection
rate_limiter
url_reputation
These already look like compatibility wrappers around Rust-owned behavior.
Still keep meaningful Python-side behavior
retry_with_backoff
encoded_exfil_detection
These still retain non-trivial Python config, fallback, or hook logic, so removing Python there is not just a packaging cleanup.
Why reconsider this
Even when the Python layer is thin, it still has cost:
- every plugin needs a Python module/object entry point and manifest wiring
- every plugin has Python packaging and import-surface conventions to maintain
- test coverage has to keep validating the Python package contract, not just the Rust behavior
- shared hook/result translation logic is duplicated or reimplemented per plugin
- the runtime story remains “Rust behind Python” rather than “native Rust plugin execution”
- architectural ownership is split: plugin behavior lives here, but the actual plugin ABI is still largely defined by the framework repo
This is not automatically wrong. The question is whether that tradeoff is still the right one now that the managed set has been extracted.
Options
Option A: Keep the current Python package ABI, but standardize the bridge harder
What this means:
- keep publishing
cpex-* Python packages
- keep Python
module:object entry points and manifest kind
- move more hook/result construction into shared Rust helpers or code generation
- reduce each plugin shim to the smallest practical compatibility surface
Pros:
- lowest-risk path
- compatible with ADR-048 and the current gateway loading model
- no framework ABI break in
mcp-context-forge
- still lets us delete a lot of repetitive per-plugin Python glue
- can be done incrementally plugin by plugin
Cons:
- Python remains part of the runtime ABI and packaging model
- we still carry Python package validation, entry-point wiring, and shim maintenance forever
- does not fully answer whether Rust-native execution should be the long-term model
Option B: Define a Rust-native plugin hook ABI, but keep this repo as the plugin home
What this means:
- the gateway/framework learns how to load and invoke managed Rust plugins directly through a stable Rust-side contract
cpex-plugins becomes the home of those Rust-native plugins and shared bridge crates
- Python packages become optional compatibility artifacts or disappear entirely for plugins that do not need Python-side behavior
Pros:
- removes per-plugin Python shims where they are only compatibility glue
- makes hook/result translation a shared native layer instead of repeated package boilerplate
- gives a cleaner architectural story for “Rust-backed managed plugins”
- still preserves the repo split that ADR-048 wanted for independent plugin ownership and release cadence
Cons:
- this is a cross-repo ABI change, not a local repo cleanup
mcp-context-forge would need new loading/runtime machinery
- release, compatibility, and version skew management may get harder before they get easier
- some plugins still have real Python logic today, so migration would not be uniform
- operators may lose the simplicity of “install normal Python packages and import plugins normally” unless we preserve that experience another way
Option C: Move the native hook/runtime boundary directly into IBM/mcp-context-forge
What this means:
- accept that the true ABI owner is the framework repo
- implement Rust-native hook loading/execution there first
- then decide whether
cpex-plugins should only hold reusable engines, or whether some managed plugin code should move back into the framework repo
Pros:
- puts ABI ownership where the runtime decisions actually live
- avoids pretending this can be solved entirely inside
cpex-plugins
- may reduce cross-repo churn if native execution is tightly coupled to gateway internals
- cleaner if plugin “loading” and hook lifecycle are framework concerns first and packaging concerns second
Cons:
- reintroduces some of the coupling ADR-048 was trying to remove
- may pull plugin release and gateway release concerns back together
- risks rebuilding the same monorepo pressure that extraction was intended to reduce
- could make independently versioned managed plugins less real in practice
Option D: Keep the current architecture and only remove redundant Python fallbacks
What this means:
- treat ADR-048 as the long-term architecture
- keep Python package entry points as the official plugin contract
- continue deleting Python fallback/parity logic where it no longer adds value
- do not pursue a Rust-native loading model
Pros:
- simplest operating model
- aligned with the current repo validator, release workflow, and ADR
- avoids a large cross-repo redesign
Cons:
- leaves the current Python shim model in place indefinitely
- keeps the architectural boundary slightly awkward for plugins that are functionally Rust-native already
- may leave us with recurring requests to “just remove the shim” without a principled answer
Recommendation
Do this in two stages instead of picking a repo move blindly.
Stage 1
In cpex-plugins, reduce the accidental Python surface as far as possible without changing the framework ABI:
- identify the exact hook/result/violation/config patterns repeated across plugins
- push more of that into shared Rust helpers or generation
- classify which plugins are truly shim-only vs which still need meaningful Python behavior
Stage 2
Open a companion architecture decision in IBM/mcp-context-forge to decide whether a direct Rust-native managed-plugin ABI should exist at all.
That decision should come before we decide to move code into the framework repo or redesign this repo around native loading.
If the framework decides the long-term ABI remains Python package imports, then the answer here is Option A/D.
If the framework decides Rust-native plugin loading is strategic, then we can choose between Option B and Option C with a real ABI owner and migration plan.
Acceptance criteria
Related
Summary
Evaluate whether the managed Rust-backed plugins in
cpex-pluginsshould keep per-plugin Python gateway shims, or whether the hook boundary should move lower so the gateway can execute them through a Rust-native contract.The current model works, but it keeps a Python package and Python import surface on every managed Rust plugin even when the actual plugin behavior is already implemented in Rust.
This issue is to make that tradeoff explicit and decide whether we should:
IBM/mcp-context-forgeand potentially relocate some of this responsibility there.Current state
Today this repo is explicitly organized around Rust plugins published as Python packages:
README.mdandDEVELOPING.mddefine the managed layout asplugins/rust/python-package/<slug>/[project.entry-points."cpex.plugins"]kindin Pythonmodule.objectformtools/plugin_catalog.pyvalidates that Python package/module/object contractIBM/mcp-context-forgeexplicitly chose PyO3-backed packages because the gateway still imports plugins as normal Python modulesThat means the Python layer is not just incidental packaging today; it is part of the current plugin ABI.
At the same time, the amount of Python-specific logic varies a lot across plugins:
Mostly thin gateway shims today
pii_filtersecrets_detectionrate_limiterurl_reputationThese already look like compatibility wrappers around Rust-owned behavior.
Still keep meaningful Python-side behavior
retry_with_backoffencoded_exfil_detectionThese still retain non-trivial Python config, fallback, or hook logic, so removing Python there is not just a packaging cleanup.
Why reconsider this
Even when the Python layer is thin, it still has cost:
This is not automatically wrong. The question is whether that tradeoff is still the right one now that the managed set has been extracted.
Options
Option A: Keep the current Python package ABI, but standardize the bridge harder
What this means:
cpex-*Python packagesmodule:objectentry points and manifestkindPros:
mcp-context-forgeCons:
Option B: Define a Rust-native plugin hook ABI, but keep this repo as the plugin home
What this means:
cpex-pluginsbecomes the home of those Rust-native plugins and shared bridge cratesPros:
Cons:
mcp-context-forgewould need new loading/runtime machineryOption C: Move the native hook/runtime boundary directly into
IBM/mcp-context-forgeWhat this means:
cpex-pluginsshould only hold reusable engines, or whether some managed plugin code should move back into the framework repoPros:
cpex-pluginsCons:
Option D: Keep the current architecture and only remove redundant Python fallbacks
What this means:
Pros:
Cons:
Recommendation
Do this in two stages instead of picking a repo move blindly.
Stage 1
In
cpex-plugins, reduce the accidental Python surface as far as possible without changing the framework ABI:Stage 2
Open a companion architecture decision in
IBM/mcp-context-forgeto decide whether a direct Rust-native managed-plugin ABI should exist at all.That decision should come before we decide to move code into the framework repo or redesign this repo around native loading.
If the framework decides the long-term ABI remains Python package imports, then the answer here is Option A/D.
If the framework decides Rust-native plugin loading is strategic, then we can choose between Option B and Option C with a real ABI owner and migration plan.
Acceptance criteria
module:object, or whether a Rust-native ABI should be introduced.IBM/mcp-context-forgeand define the migration boundary.IBM/mcp-context-forgeas part of that decision, instead of letting the boundary drift implicitly.Related
IBM/mcp-context-forgeADR-048: Extract Rust-Backed Plugins First and Preserve Python Examples Separately