Skip to content

feat(proxy): entry-level URL rewriting via proxy.url_rewrites - #878

Closed
jarvis9443 wants to merge 3 commits into
feat/mcp-scoped-endpointfrom
feat/url-rewrites
Closed

feat(proxy): entry-level URL rewriting via proxy.url_rewrites#878
jarvis9443 wants to merge 3 commits into
feat/mcp-scoped-endpointfrom
feat/url-rewrites

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Stacked on #875 (the /mcp/{server} scoped endpoint); only the top commit is new here.

What

Adds proxy.url_rewrites: an ordered list of entry-level URL rewrite rules applied to every proxy-listener request before route matching (the admin and metrics listeners are unaffected).

proxy:
  url_rewrites:
    - name: per-server-mcp-compat
      match: "^/mcp-servers/([^/]+)/mcp$"
      rewrite: "/mcp/$1"
  • The first rule whose match regex matches the request path rewrites it — once, no cascading — and the request then flows through the normal endpoint (auth, ACL, quota, metrics labelling) exactly as if the client had sent the rewritten path. A miss leaves the request untouched.
  • rewrite replaces the matched portion of the path; $1/${name} expand capture groups; the query string is preserved as sent.
  • An invalid regex fails startup (Config::validate), so a typo surfaces at boot instead of as every legacy request 404ing. A template that assembles an invalid path at runtime logs a warning naming the rule and leaves the request unrewritten.
  • Rules are compiled once at boot; the middleware wrapper is only built when rules are configured, so deployments without rules pay nothing.

Implementation note: axum's Router::layer middleware runs after route matching, so a URI rewritten there could never change which route matches. The rewrite therefore wraps the whole router as the fallback of an outer router, giving it a genuine pre-routing seat.

Why

Lets operators map legacy URL shapes onto AISIX endpoints without client changes. The flagship scenario (api7/AISIX-Cloud#1219): clients migrating from gateways that expose one URL per MCP server (/mcp-servers/{service}/{path}) keep their configured URLs and original tool names — one rule maps the URL onto the /mcp/{server} endpoint from #875, and the whole existing governance chain applies unchanged.

Design comparison (per repo rule): mainstream gateways all ship a regex path-rewrite primitive with matched-portion replacement and capture-group templates (route-plugin, per-route rewrite, or middleware forms). Ours differs in placement only — a gateway-global ordered rule list instead of per-route config — because AISIX's routes are fixed built-in endpoints and the layer's purpose is mapping external URL space onto them; first-match-wins order replaces per-route attachment. LiteLLM offers no operator-configurable equivalent (its per-server MCP alias route is an internal fixed rewrite of the same shape), so the APISIX-style rewrite plugins are the reference baseline here.

Rewriting cannot bypass governance: it only re-targets which proxy endpoint serves the request, and every endpoint enforces its own auth/ACL/quota after the rewrite; the admin surface lives on a separate listener the layer never touches.

Tests

  • crates/aisix-proxy/src/rewrite.rs — unit + router-level: capture groups, matched-portion semantics, named/braced references, query preservation, invalid-path fallback, first-rule-wins through the real router, no-rules passthrough.
  • crates/aisix-core/src/config.rs — config load + invalid-regex boot rejection.
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts — real binary + etcd + real MCP upstream: the full migration scenario (legacy per-server URL + original tool name end to end), generic non-MCP mapping, query survival, miss-passthrough (canonical paths intact, unmatched legacy tails 404).

config.example.yaml / config.managed.yaml document the block. Fixes api7/AISIX-Cloud#1219.

An ordered list of {match, rewrite} regex rules applied to every
proxy-listener request before route matching (admin/metrics listeners
unaffected): the first matching rule rewrites the path once — no
cascading — and the request then flows through the normal endpoint
(auth, ACL, quota, metrics labelling) as if the client had sent the
rewritten path. Replacement substitutes the matched portion with
$1/${name} capture-group expansion; the query string is preserved; a
miss leaves the request untouched. Invalid regexes fail startup.

Because axum's Router::layer middleware runs after route matching, the
rewrite gets its pre-routing seat by wrapping the whole router as the
fallback of an outer router; the wrapper is only built when rules are
configured, so the default path pays nothing.

Lets operators map legacy URL shapes onto AISIX endpoints without
client changes — e.g. per-server MCP paths like /mcp-servers/{svc}/mcp
onto the /mcp/{server} endpoint, completing the migration scenario of
api7/AISIX-Cloud#1219 together with the scoped-endpoint PR.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff474112-0983-4433-a3b4-449b7c7a4ae3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 deleted the branch feat/mcp-scoped-endpoint August 4, 2026 09:46
@jarvis9443 jarvis9443 closed this Aug 4, 2026
@jarvis9443

Copy link
Copy Markdown
Contributor Author

GitHub auto-closed this when the stacked base branch was deleted on #875's merge; continued as #881 (same branch, based on main, plus review-driven hardening: template validation at boot, env JSON form, raw-path matching docs/tests).

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