Skip to content

Add to_rust_native_policy() method to RetryWithBackoffPlugin #21

@jonpspri

Description

@jonpspri

Background

The ContextForge gateway (IBM/mcp-context-forge) has a Rust runtime fast path (tools_rust/mcp_runtime) for tool invocation. When RetryWithBackoffPlugin is the only active tool_post_invoke hook, the gateway can execute the tool natively in Rust and apply the retry policy without round-tripping through Python.

To do that, the gateway currently has to:

  1. Import RetryConfig from this package directly
  2. Construct it from a raw config dict
  3. Apply the gateway's max_tool_retries ceiling
  4. Merge per-tool overrides
  5. Reject if check_text_content=True (Rust runtime can't evaluate that)
  6. Translate the result into the wire format Rust expects

See mcpgateway/services/tool_service.py:_build_rust_native_tool_post_invoke_retry_policy.

This is a leaky abstraction — the gateway core has hardcoded knowledge of this plugin's internal config schema. We'd like to move the transformation into the plugin itself so the gateway only depends on a stable protocol method.

Proposed change

Add a to_rust_native_policy() method to RetryWithBackoffPlugin:

class RetryWithBackoffPlugin(Plugin):
    def to_rust_native_policy(self, tool_name: str, ceiling: int) -> Optional[dict]:
        '''Translate this plugin's effective config into the Rust runtime wire format.

        Args:
            tool_name: The tool being invoked, used to look up per-tool overrides.
            ceiling: The gateway's hard maximum on retries (settings.max_tool_retries).
                     The plugin must clamp its own max_retries to this value.

        Returns:
            A dict matching the Rust runtime wire format, or None if this plugin
            invocation is not eligible for native Rust execution (e.g. when
            check_text_content=True, which requires Python text inspection).

        Wire format (must be stable):
            {
                'kind': 'retry_with_backoff',
                'maxRetries': int,
                'backoffBaseMs': int,
                'maxBackoffMs': int,
                'retryOnStatus': list[int],
                'jitter': bool,
            }
        '''

Reference implementation

The translation logic the gateway currently performs (which should move into this method):

effective_cfg = RetryConfig(**(self.config.config or {}))
if effective_cfg.max_retries > ceiling:
    effective_cfg = effective_cfg.model_copy(update={'max_retries': ceiling})

overrides = effective_cfg.tool_overrides.get(tool_name)
if overrides:
    merged_cfg = effective_cfg.model_dump()
    merged_cfg.update(overrides)
    merged_cfg.pop('tool_overrides', None)
    effective_cfg = RetryConfig(**merged_cfg)
    if effective_cfg.max_retries > ceiling:
        effective_cfg = effective_cfg.model_copy(update={'max_retries': ceiling})

if effective_cfg.check_text_content:
    return None  # not eligible

return {
    'kind': 'retry_with_backoff',
    'maxRetries': int(effective_cfg.max_retries),
    'backoffBaseMs': int(effective_cfg.backoff_base_ms),
    'maxBackoffMs': int(effective_cfg.max_backoff_ms),
    'retryOnStatus': list(effective_cfg.retry_on_status),
    'jitter': bool(effective_cfg.jitter),
}

Wire format stability

The returned dict format is consumed by tools_rust/mcp_runtime in the gateway repo. Treat the keys (kind, maxRetries, backoffBaseMs, maxBackoffMs, retryOnStatus, jitter) as a stable wire contract — additions are fine, removals/renames are breaking.

Acceptance criteria

  • RetryWithBackoffPlugin.to_rust_native_policy(tool_name: str, ceiling: int) -> Optional[dict] is implemented.
  • Unit tests cover: simple config, ceiling clamping, per-tool overrides, override clamping, check_text_content=True returning None.
  • Released to PyPI as a minor version bump.

Linked gateway issue

Once this ships, the gateway will refactor to use the new method and drop its direct cpex_retry_with_backoff import:

Context

Surfaced during PR review of IBM/mcp-context-forge#3965, which migrated six in-tree plugins (including retry_with_backoff) from the gateway repo to standalone PyPI packages. The gateway currently uses a try/except ImportError guard as a tactical fix; this issue tracks the architectural cleanup.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

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