Skip to content

Commit 5fe2266

Browse files
committed
feat: Add UI5_MCP_SERVER_CDN_URL env var to override CDN base URL
Allow users in restricted environments to override the default CDN URLs for OpenUI5 and SAPUI5 with a custom mirror via a single environment variable.
1 parent 231ff0a commit 5fe2266

3 files changed

Lines changed: 127 additions & 13 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ The UI5 MCP server can be configured using the following environment variables.
141141
* Description: Set to any value to disable structured content in the MCP server responses.
142142
* **`UI5_MCP_SERVER_RESPONSE_NO_RESOURCES`**:
143143
* Description: Set to any value to disable [resources](https://modelcontextprotocol.io/specification/2025-06-18/server/resources) in the MCP server responses. This is useful for [clients that do not support resources](https://modelcontextprotocol.io/clients), such as Cursor or the Gemini CLI.
144+
* **`UI5_MCP_SERVER_CDN_URL`**:
145+
* Default Value: `https://sdk.openui5.org` for OpenUI5 and `https://ui5.sap.com` for SAPUI5
146+
* Description: Override the base URL used for fetching UI5 resources from the CDN. For example: `https://example.com`. When set, this URL is used for both OpenUI5 and SAPUI5 resources. The value must be a valid URL. Note that the project's version will appended to the URL automatically. This means that at runtime, the URL might look like this: `https://example.com/1.120.0/resources/[...]`.
144147
* **`UI5_LOG_LVL`**:
145148
* Default Value: `info`
146149
* Description: Internal [log level](https://ui5.github.io/cli/stable/pages/Troubleshooting/#changing-the-log-level): `silent`, `error`, `warn`, `info`, `perf`, `verbose`, `silly`

src/utils/cdnHelper.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,55 @@
11
import fetch from "make-fetch-happen";
2+
import {getLogger} from "@ui5/logger";
23
import {InvalidInputError} from "../utils.js";
34
import {Ui5Framework} from "./ui5Framework.js";
45

6+
const log = getLogger("utils:cdnHelper");
7+
8+
function getCustomCdnUrl(): string | undefined {
9+
const value = process.env.UI5_MCP_SERVER_CDN_URL;
10+
if (!value) {
11+
return undefined;
12+
}
13+
let url = value.trim();
14+
if (!url) {
15+
return undefined;
16+
}
17+
// Strip trailing slashes
18+
url = url.replace(/\/+$/, "");
19+
// Validate URL format
20+
try {
21+
new URL(url);
22+
} catch {
23+
throw new InvalidInputError(
24+
`Invalid URL '${value}' in UI5_MCP_SERVER_CDN_URL: ` +
25+
`The value must be a valid URL (e.g., "https://my-cdn.example.com")`
26+
);
27+
}
28+
log.info(`Using custom CDN URL from UI5_MCP_SERVER_CDN_URL: ${url}`);
29+
return url;
30+
}
31+
532
/**
633
* Get the base URL for the UI5 CDN based on the framework
734
* @param frameworkName The UI5 framework (OpenUI5 or SAPUI5)
835
* @returns The CDN base URL
936
*/
1037
export function getBaseUrl(frameworkName: Ui5Framework, frameworkVersion?: string): string {
11-
let baseUrl;
12-
switch (frameworkName) {
13-
case "OpenUI5":
14-
baseUrl = "https://sdk.openui5.org";
15-
break;
16-
case "SAPUI5":
17-
baseUrl = "https://ui5.sap.com";
18-
break;
19-
default:
20-
// This should never happen due to TypeScript's type checking,
21-
// but we handle it anyway for runtime safety
22-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
23-
throw new InvalidInputError(`Unknown framework: ${frameworkName}`);
38+
let baseUrl = getCustomCdnUrl();
39+
if (!baseUrl) {
40+
switch (frameworkName) {
41+
case "OpenUI5":
42+
baseUrl = "https://sdk.openui5.org";
43+
break;
44+
case "SAPUI5":
45+
baseUrl = "https://ui5.sap.com";
46+
break;
47+
default:
48+
// This should never happen due to TypeScript's type checking,
49+
// but we handle it anyway for runtime safety
50+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
51+
throw new InvalidInputError(`Unknown framework: ${frameworkName}`);
52+
}
2453
}
2554
if (frameworkVersion) {
2655
baseUrl += `/${frameworkVersion}`;

test/lib/utils/cdnHelper.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,31 @@ const test = anyTest as TestFn<{
1010
fetchCdn: typeof import("../../../src/utils/cdnHelper.js").fetchCdn;
1111
fetchCdnRaw: typeof import("../../../src/utils/cdnHelper.js").fetchCdnRaw;
1212
fetchStub: sinonGlobal.SinonStub;
13+
loggerMock: {
14+
info: sinonGlobal.SinonStub;
15+
};
16+
originalEnv: NodeJS.ProcessEnv;
1317
}>;
1418

1519
test.beforeEach(async (t) => {
1620
t.context.sinon = sinonGlobal.createSandbox();
21+
t.context.originalEnv = {...process.env};
1722

1823
// Create a stub for global fetch
1924
const fetchStub = t.context.sinon.stub();
2025
t.context.fetchStub = fetchStub;
2126

27+
const loggerMock = {
28+
info: t.context.sinon.stub(),
29+
};
30+
t.context.loggerMock = loggerMock;
31+
2232
// Import the module with mocked dependencies
2333
const {getBaseUrl, fetchCdn, fetchCdnRaw} = await esmock("../../../src/utils/cdnHelper.js", {
2434
"make-fetch-happen": fetchStub,
35+
"@ui5/logger": {
36+
getLogger: t.context.sinon.stub().returns(loggerMock),
37+
},
2538
});
2639

2740
t.context.getBaseUrl = getBaseUrl;
@@ -30,25 +43,29 @@ test.beforeEach(async (t) => {
3043
});
3144

3245
test.afterEach.always((t) => {
46+
process.env = t.context.originalEnv;
3347
t.context.sinon.restore();
3448
});
3549

3650
test.serial("getBaseUrl returns correct URL for OpenUI5", (t) => {
3751
const {getBaseUrl} = t.context;
52+
delete process.env.UI5_MCP_SERVER_CDN_URL;
3853

3954
const url = getBaseUrl("OpenUI5");
4055
t.is(url, "https://sdk.openui5.org");
4156
});
4257

4358
test.serial("getBaseUrl returns correct URL for SAPUI5", (t) => {
4459
const {getBaseUrl} = t.context;
60+
delete process.env.UI5_MCP_SERVER_CDN_URL;
4561

4662
const url = getBaseUrl("SAPUI5");
4763
t.is(url, "https://ui5.sap.com");
4864
});
4965

5066
test.serial("getBaseUrl throws for unknown framework", (t) => {
5167
const {getBaseUrl} = t.context;
68+
delete process.env.UI5_MCP_SERVER_CDN_URL;
5269

5370
const error = t.throws(() => getBaseUrl("unknown-framework" as Ui5Framework));
5471
t.true(error instanceof InvalidInputError);
@@ -57,11 +74,76 @@ test.serial("getBaseUrl throws for unknown framework", (t) => {
5774

5875
test.serial("getBaseUrl appends version when provided", (t) => {
5976
const {getBaseUrl} = t.context;
77+
delete process.env.UI5_MCP_SERVER_CDN_URL;
6078

6179
const url = getBaseUrl("OpenUI5", "1.120.0");
6280
t.is(url, "https://sdk.openui5.org/1.120.0");
6381
});
6482

83+
test.serial("getBaseUrl uses custom CDN URL for OpenUI5", (t) => {
84+
const {getBaseUrl, loggerMock} = t.context;
85+
process.env.UI5_MCP_SERVER_CDN_URL = "https://internal-mirror.corp.com";
86+
87+
const url = getBaseUrl("OpenUI5");
88+
t.is(url, "https://internal-mirror.corp.com");
89+
t.true(loggerMock.info.calledWith(
90+
"Using custom CDN URL from UI5_MCP_SERVER_CDN_URL: https://internal-mirror.corp.com"
91+
));
92+
});
93+
94+
test.serial("getBaseUrl uses custom CDN URL for SAPUI5", (t) => {
95+
const {getBaseUrl, loggerMock} = t.context;
96+
process.env.UI5_MCP_SERVER_CDN_URL = "https://internal-mirror.corp.com";
97+
98+
const url = getBaseUrl("SAPUI5");
99+
t.is(url, "https://internal-mirror.corp.com");
100+
t.true(loggerMock.info.calledWith(
101+
"Using custom CDN URL from UI5_MCP_SERVER_CDN_URL: https://internal-mirror.corp.com"
102+
));
103+
});
104+
105+
test.serial("getBaseUrl appends version to custom CDN URL", (t) => {
106+
const {getBaseUrl} = t.context;
107+
process.env.UI5_MCP_SERVER_CDN_URL = "https://internal-mirror.corp.com";
108+
109+
const url = getBaseUrl("OpenUI5", "1.120.0");
110+
t.is(url, "https://internal-mirror.corp.com/1.120.0");
111+
});
112+
113+
test.serial("getBaseUrl strips trailing slashes from custom CDN URL", (t) => {
114+
const {getBaseUrl} = t.context;
115+
process.env.UI5_MCP_SERVER_CDN_URL = "https://internal-mirror.corp.com///";
116+
117+
const url = getBaseUrl("OpenUI5");
118+
t.is(url, "https://internal-mirror.corp.com");
119+
});
120+
121+
test.serial("getBaseUrl throws for invalid custom CDN URL", (t) => {
122+
const {getBaseUrl} = t.context;
123+
process.env.UI5_MCP_SERVER_CDN_URL = "not-a-valid-url";
124+
125+
const error = t.throws(() => getBaseUrl("OpenUI5"));
126+
t.true(error instanceof InvalidInputError);
127+
t.regex(error.message, /Invalid URL 'not-a-valid-url' in UI5_MCP_SERVER_CDN_URL/);
128+
});
129+
130+
test.serial("getBaseUrl ignores empty custom CDN URL and uses default", (t) => {
131+
const {getBaseUrl, loggerMock} = t.context;
132+
process.env.UI5_MCP_SERVER_CDN_URL = "";
133+
134+
const url = getBaseUrl("OpenUI5");
135+
t.is(url, "https://sdk.openui5.org");
136+
t.false(loggerMock.info.called);
137+
});
138+
139+
test.serial("getBaseUrl trims whitespace from custom CDN URL", (t) => {
140+
const {getBaseUrl} = t.context;
141+
process.env.UI5_MCP_SERVER_CDN_URL = " https://internal-mirror.corp.com ";
142+
143+
const url = getBaseUrl("SAPUI5");
144+
t.is(url, "https://internal-mirror.corp.com");
145+
});
146+
65147
test.serial("fetchCdnRaw returns response on success", async (t) => {
66148
const {fetchCdnRaw, fetchStub} = t.context;
67149

0 commit comments

Comments
 (0)