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-compatmatch: "^/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

coderabbitaiBot 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-endpointAugust 4, 2026 09:46
@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

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 freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #878 · api7/aisix · GitHub
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-compatmatch: "^/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

coderabbitaiBot 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-endpointAugust 4, 2026 09:46
@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

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 freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #878 · api7/aisix · GitHub
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-compatmatch: "^/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

coderabbitaiBot 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-endpointAugust 4, 2026 09:46
@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

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 freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #878 · api7/aisix · GitHub
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-compatmatch: "^/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

coderabbitaiBot 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-endpointAugust 4, 2026 09:46
@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

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 freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #878 · api7/aisix · GitHub
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-compatmatch: "^/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

coderabbitaiBot 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-endpointAugust 4, 2026 09:46
@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

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 freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #878 · api7/aisix · GitHub
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-compatmatch: "^/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

coderabbitaiBot 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-endpointAugust 4, 2026 09:46
@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

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 freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #878 · api7/aisix · GitHub
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-compatmatch: "^/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

coderabbitaiBot 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-endpointAugust 4, 2026 09:46
@jarvis9443

Copy link
Copy Markdown
ContributorAuthor

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 freeto 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

@jarvis9443