Skip to content

[CHORE]: Refactor tool_service.py to remove direct cpex_retry_with_backoff import #4116

@jonpspri

Description

@jonpspri

Background

mcpgateway/services/tool_service.py:_build_rust_native_tool_post_invoke_retry_policy currently imports RetryConfig directly from the optional cpex-retry-with-backoff PyPI package:

try:
    from cpex_retry_with_backoff import RetryConfig
except ImportError:
    return (None, True)

This is a leaky abstraction: the gateway core knows about the internal config schema of an opt-in plugin. The import exists solely to translate the plugin's pydantic config into the wire format that tools_rust/mcp_runtime expects when taking the Rust fast path for tool post-invoke.

The current try/except ImportError guard is correct and safe (the import is only reached when the plugin is actively loaded, which means the cpex package must already be installed), but it bakes a cross-package dependency into gateway core that we'd rather not have long-term.

Why not move cpex-retry-with-backoff to a core dependency?

We considered this and decided against it. cpex-retry-with-backoff sits in the same architectural tier as the other five cpex-* plugins (PII filter, secrets detection, URL reputation, encoded exfil detection, rate limiter) — all are opt-in via the [plugins] extra. Promoting only one breaks the symmetry and forces operators who don't use retry behavior to install the package anyway. The whole point of migrating plugins to PyPI was to decouple their release cadence from the gateway core.

Proposed refactor

Have the plugin expose the Rust-native policy transformation itself, so the gateway never needs to import the plugin's internal types:

# In cpex_retry_with_backoff (upstream change — see linked issue):
class RetryWithBackoffPlugin(Plugin):
    def to_rust_native_policy(self, tool_name: str, ceiling: int) -> Optional[dict]:
        '''Return the Rust runtime wire format, or None if not eligible (e.g. check_text_content=True).'''
        ...

Then tool_service.py simplifies to:

def _build_rust_native_tool_post_invoke_retry_policy(self, plugin_manager, tool_name, hook_global_context):
    # ... existing hook discovery / filtering logic unchanged ...

    if len(active_hook_refs) != 1 or active_hook_refs[0].plugin_ref.name != "RetryWithBackoffPlugin":
        return (None, True)

    retry_hook = active_hook_refs[0]
    policy = retry_hook.plugin_ref.plugin.to_rust_native_policy(tool_name, settings.max_tool_retries)
    return (policy, policy is None)

Benefits:

  • No import cpex_retry_with_backoff anywhere in gateway core
  • No try/except ImportError defensive guard
  • Plugin owns its own config schema and wire-format translation (single source of truth)
  • Future post-invoke plugins can implement the same to_rust_native_policy() protocol without touching tool_service.py at all

Dependency

This refactor requires an upstream change in the cpex-retry-with-backoff package. Tracking issue:

Once that ships and is published to PyPI, bump the minimum version in pyproject.toml [plugins] extra and apply the gateway-side refactor in this issue.

Acceptance criteria

  • RetryWithBackoffPlugin.to_rust_native_policy() is available in a published cpex-retry-with-backoff release (see linked cpex-plugins issue).
  • pyproject.toml [plugins] extra requires the new minimum version.
  • mcpgateway/services/tool_service.py no longer imports cpex_retry_with_backoff (verified by grep).
  • Existing Rust fast-path tests still pass; behavior is unchanged for installed-plugin and missing-plugin cases.

Context

Surfaced during PR review of #3965 (in-tree plugin → PyPI migration). The lazy-import-with-fallback was the right tactical fix for that PR; this issue tracks the architectural cleanup as a follow-up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    choreLinting, formatting, dependency hygiene, or project maintenance choresenhancementNew feature or requesttriageIssues / Features awaiting triage

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions