Skip to content

Commit e214dee

Browse files
Merge branch 'develop' into feature/address-autocompletion
2 parents f882b65 + 598e83e commit e214dee

File tree

5 files changed

+531
-13
lines changed

5 files changed

+531
-13
lines changed

packages/pwa-kit-mcp/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## 0.5.0-dev (Oct 24, 2025)
22
- Retired the pwakit_create_component tool [#3437](https://github.com/SalesforceCommerceCloud/pwa-kit/issues/3437)
3+
- Added the fallback path for the custom Api tool [#3458] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3458)
34

45
## 0.4.0 (Oct 24, 2025)
56
- Unexposed the extra parameters on create_page tool. [#3359] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3359), [#3379] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3379)

packages/pwa-kit-mcp/README.md

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ The PWA Kit MCP Server offers the following intelligent tools tailored to Salesf
5151
*Example: `How do I get a product?`*
5252

5353
* **`scapi_custom_api_discovery`**:
54-
Discovers custom SCAPI APIs registered on BM, and fetches the schema of those APIs. Requires credential configuration described in the 🔧 Configuration Options section.
54+
Discovers custom SCAPI APIs registered on BM, and fetches the schema of those APIs. Requires credential configuration described in the 🔧 Configuration Options section.
5555
*Note: Ensure your API Client has access to your instance and has 'sfcc.custom-apis' as allowed scope*
5656

57+
**Fallback Mode**: If SFCC credentials are not available, the tool will search for `api.json` and `schema.yaml` files locally in the following order:
58+
1. `SFCC_CARTRIDGE_PATH` environment variable (if set) - searches recursively up to 10 levels deep
59+
2. `PWA_STOREFRONT_APP_PATH` - searches current directory and parent directories (up to filesystem root or home directory, max 10 levels), then recursively down 10 levels in each
60+
5761
*Custom API DX Endpoint Documentation*: [https://developer.salesforce.com/docs/commerce/commerce-api/references/custom-apis?meta=getEndpoints](https://developer.salesforce.com/docs/commerce/commerce-api/references/custom-apis?meta=getEndpoints)
5862

5963

@@ -139,7 +143,55 @@ SFCC_CLIENT_ID=your-client-id
139143
SFCC_CLIENT_SECRET=your-client-secret
140144
```
141145

142-
**Note:** Environment variables take precedence over`dw.json` values if both are provided.
146+
**Note:** Environment variables take precedence over `dw.json` values if both are provided.
147+
148+
#### Option 3: Local Custom API Files (Fallback for Custom API Discovery)
149+
150+
For the `scapi_custom_api_discovery` tool, if SFCC credentials are not available, you can provide local custom API files:
151+
152+
**Method 1: Direct Path** - Set the `SFCC_CARTRIDGE_PATH` environment variable to point to your custom API cartridge directory:
153+
154+
```json
155+
{
156+
"mcpServers": {
157+
"pwa-kit": {
158+
"command": "npx",
159+
"args": ["-y", "@salesforce/pwa-kit-mcp"],
160+
"env": {
161+
"PWA_STOREFRONT_APP_PATH": "{{path-to-app-directory}}",
162+
"SFCC_CARTRIDGE_PATH": "/path/to/your/cartridge"
163+
}
164+
}
165+
}
166+
}
167+
```
168+
169+
**Method 2: Auto-discovery** - If `SFCC_CARTRIDGE_PATH` is not set, the tool will automatically search for `api.json` and `schema.yaml` files starting from `PWA_STOREFRONT_APP_PATH` and traversing up through parent directories until it reaches the filesystem root or home directory (max 10 levels). At each directory level, it searches recursively down through subdirectories (up to 10 levels deep).
170+
171+
**File Structure Expected:**
172+
```
173+
your-custom-api-directory/
174+
├── api.json # Custom API metadata
175+
└── schema.yaml # OpenAPI schema (optional)
176+
```
177+
178+
**Example `api.json`:**
179+
```json
180+
{
181+
"apiName": "reviews",
182+
"apiVersion": "v1",
183+
"cartridgeName": "plugin_custom_api_intro",
184+
"endpointPath": "reviews",
185+
"httpMethod": "GET",
186+
"securityScheme": "bearer",
187+
"baseUrl": "https://your-shortcode.api.commercecloud.salesforce.com/custom/reviews/v1"
188+
}
189+
```
190+
191+
**Search Priority:**
192+
1. SFCC credentials (dw.json or environment variables)
193+
2. `SFCC_CARTRIDGE_PATH` environment variable - searches recursively up to 10 levels deep in subdirectories
194+
3. `PWA_STOREFRONT_APP_PATH` and up to 5 parent directories - searches recursively up to 10 levels deep in subdirectories at each level
143195

144196
## 📊 Telemetry
145197

packages/pwa-kit-mcp/src/tools/custom-api-discovery.js

Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import {loadConfig, getOAuthToken, callCustomApiDxEndpoint, logMCPMessage} from '../utils/utils.js'
7+
import {
8+
loadConfig,
9+
getOAuthToken,
10+
callCustomApiDxEndpoint,
11+
logMCPMessage,
12+
loadCustomApiFromFallbackPath
13+
} from '../utils/utils.js'
814
import {
915
parseWebDAVDirectories,
1016
parseWebDAVResponse,
@@ -91,15 +97,28 @@ function fetchAndValidateConfigs() {
9197
const config = loadConfig()
9298
const {clientId, clientSecret, organizationId, instanceId, shortCode, hostname} = config
9399

94-
// Validate configuration fields
95-
const nullConfigFields = Object.entries(config)
96-
.filter(([, value]) => value === null || value === undefined)
97-
.map(([key]) => key)
100+
// Define which fields are critical for SFCC API connection
101+
const criticalFields = ['clientId', 'clientSecret', 'organizationId', 'instanceId']
102+
103+
// Check which critical fields are missing
104+
const missingCriticalFields = criticalFields.filter(
105+
(field) => !config[field] || config[field].trim() === ''
106+
)
107+
108+
// If all critical fields are missing, use fallback mode (local file search)
109+
if (missingCriticalFields.length === criticalFields.length) {
110+
return {isFallback: true}
111+
}
98112

99-
if (nullConfigFields.length > 0) {
100-
throw new Error(`Required configuration fields are null: ${nullConfigFields.join(', ')}`)
113+
// If some (but not all) critical fields are missing, throw an error with helpful message
114+
if (missingCriticalFields.length > 0) {
115+
throw new Error(
116+
`Required SFCC API credentials are missing: ${missingCriticalFields.join(', ')}. ` +
117+
`Either provide all credentials or use SFCC_CARTRIDGE_PATH environment variable for local fallback.`
118+
)
101119
}
102120

121+
// All critical fields present, return config
103122
return {clientId, clientSecret, organizationId, instanceId, shortCode, hostname}
104123
}
105124

@@ -207,6 +226,37 @@ async function searchRecursivelyForApiName(
207226
return {searchResults}
208227
}
209228

229+
/**
230+
* Processes custom API data from local fallback path
231+
* @returns {Array} Array of processed custom API entries
232+
*/
233+
function processFallbackCustomApi() {
234+
const fallbackData = loadCustomApiFromFallbackPath()
235+
236+
if (!fallbackData) {
237+
return []
238+
}
239+
240+
const {apiJson, schemaYaml, source} = fallbackData
241+
242+
// Transform api.json structure to match expected output format
243+
const processedEntry = {
244+
apiName: apiJson.apiName || apiJson.name,
245+
apiVersion: apiJson.apiVersion || apiJson.version,
246+
cartridgeName: apiJson.cartridgeName || 'local-fallback',
247+
endpointPath: apiJson.endpointPath || apiJson.path,
248+
httpMethod: apiJson.httpMethod || apiJson.method,
249+
status: 'active',
250+
securityScheme: apiJson.securityScheme || 'unknown',
251+
siteId: apiJson.siteId || null,
252+
baseUrl: apiJson.baseUrl || 'local-fallback',
253+
schema: schemaYaml,
254+
source: source
255+
}
256+
257+
return [processedEntry]
258+
}
259+
210260
export default {
211261
name: 'scapi_custom_api_discovery',
212262
description:
@@ -215,8 +265,67 @@ export default {
215265
fn: async () => {
216266
let dxEndpointResponse = null
217267
let activeCodeVersion = null
218-
const {clientId, clientSecret, organizationId, instanceId, shortCode, hostname} =
219-
fetchAndValidateConfigs()
268+
269+
const config = fetchAndValidateConfigs()
270+
271+
// Handle fallback scenario
272+
if (config.isFallback) {
273+
try {
274+
const fallbackApis = processFallbackCustomApi()
275+
276+
if (fallbackApis.length === 0) {
277+
return {
278+
content: [
279+
{
280+
type: 'text',
281+
text: JSON.stringify(
282+
{
283+
metadata: {
284+
fallback: true,
285+
activeCodeVersion: null,
286+
timestamp: new Date().toISOString(),
287+
totalApis: 0,
288+
message:
289+
'No SFCC configuration found (dw.json or env vars) and no local fallback path available. Please configure SFCC_CARTRIDGE_PATH environment variable or PWA_STOREFRONT_APP_PATH, or provide SFCC credentials.'
290+
},
291+
customApis: []
292+
},
293+
null,
294+
2
295+
)
296+
}
297+
]
298+
}
299+
}
300+
301+
return {
302+
content: [
303+
{
304+
type: 'text',
305+
text: JSON.stringify(
306+
{
307+
metadata: {
308+
fallback: true,
309+
activeCodeVersion: 'local-filesystem',
310+
timestamp: new Date().toISOString(),
311+
totalApis: fallbackApis.length,
312+
source: fallbackApis[0]?.source || 'local-fallback'
313+
},
314+
customApis: fallbackApis
315+
},
316+
null,
317+
2
318+
)
319+
}
320+
]
321+
}
322+
} catch (error) {
323+
return toErrorResponse(error, [])
324+
}
325+
}
326+
327+
// Continue with existing SFCC API logic
328+
const {clientId, clientSecret, organizationId, instanceId, shortCode, hostname} = config
220329
const customApiHost = `${shortCode}.api.commercecloud.salesforce.com`
221330
const oauthScope = `SALESFORCE_COMMERCE_API:${instanceId} sfcc.custom-apis`
222331

packages/pwa-kit-mcp/src/tools/custom-api-discovery.test.js

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ jest.mock('../utils/utils.js', () => ({
2121
throwCustomApiError: jest.fn(),
2222
getOAuthToken: jest.fn(),
2323
callCustomApiDxEndpoint: jest.fn(),
24-
logMCPMessage: jest.fn()
24+
logMCPMessage: jest.fn(),
25+
loadCustomApiFromFallbackPath: jest.fn()
2526
}))
2627

28+
// Import mocked utilities
29+
import {loadConfig, loadCustomApiFromFallbackPath} from '../utils/utils.js'
30+
2731
// Mock fetch globally
2832
global.fetch = jest.fn()
2933

@@ -380,7 +384,117 @@ describe('CustomApiTool', () => {
380384
})
381385

382386
await expect(CustomApiTool.fn()).rejects.toThrow(
383-
'Required configuration fields are null: clientSecret, organizationId'
387+
'Required SFCC API credentials are missing: clientSecret, organizationId'
384388
)
385389
})
390+
391+
describe('Fallback path scenarios', () => {
392+
beforeEach(() => {
393+
jest.clearAllMocks()
394+
})
395+
396+
test('should use fallback path when all SFCC config fields are null', async () => {
397+
// Mock all config fields as null (triggers fallback)
398+
loadConfig.mockReturnValue({
399+
clientId: null,
400+
clientSecret: null,
401+
organizationId: null,
402+
instanceId: null,
403+
shortCode: null,
404+
hostname: null
405+
})
406+
407+
// Mock fallback data
408+
loadCustomApiFromFallbackPath.mockReturnValue({
409+
apiJson: {
410+
apiName: 'reviews',
411+
apiVersion: 'v1',
412+
cartridgeName: 'plugin_custom_api_intro',
413+
endpointPath: 'reviews',
414+
httpMethod: 'GET',
415+
securityScheme: 'bearer',
416+
baseUrl: 'https://test.api.commercecloud.salesforce.com/custom/reviews/v1'
417+
},
418+
schemaYaml: 'openapi: 3.0.0...',
419+
source: 'SFCC_CARTRIDGE_PATH'
420+
})
421+
422+
const result = await CustomApiTool.fn()
423+
424+
expect(result.content[0].type).toBe('text')
425+
const parsedResult = JSON.parse(result.content[0].text)
426+
expect(parsedResult.metadata.fallback).toBe(true)
427+
expect(parsedResult.metadata.source).toBe('SFCC_CARTRIDGE_PATH')
428+
expect(parsedResult.customApis).toHaveLength(1)
429+
expect(parsedResult.customApis[0].apiName).toBe('reviews')
430+
})
431+
432+
test('should return empty response when fallback path returns no data', async () => {
433+
// Mock all config fields as null (triggers fallback)
434+
loadConfig.mockReturnValue({
435+
clientId: null,
436+
clientSecret: null,
437+
organizationId: null,
438+
instanceId: null,
439+
shortCode: null,
440+
hostname: null
441+
})
442+
443+
// Mock fallback returning null
444+
loadCustomApiFromFallbackPath.mockReturnValue(null)
445+
446+
const result = await CustomApiTool.fn()
447+
448+
expect(result.content[0].type).toBe('text')
449+
const parsedResult = JSON.parse(result.content[0].text)
450+
expect(parsedResult.metadata.fallback).toBe(true)
451+
expect(parsedResult.metadata.totalApis).toBe(0)
452+
expect(parsedResult.customApis).toEqual([])
453+
expect(parsedResult.metadata.message).toContain('No SFCC configuration found')
454+
})
455+
456+
test('should use fallback when all critical fields are missing', async () => {
457+
// Mock all 4 critical fields as null (triggers fallback)
458+
loadConfig.mockReturnValue({
459+
clientId: null,
460+
clientSecret: null,
461+
organizationId: null,
462+
instanceId: null,
463+
shortCode: 'test',
464+
hostname: 'test.com'
465+
})
466+
467+
loadCustomApiFromFallbackPath.mockReturnValue({
468+
apiJson: {
469+
apiName: 'test-api',
470+
apiVersion: 'v1'
471+
},
472+
schemaYaml: null,
473+
source: 'PWA_STOREFRONT_APP_PATH'
474+
})
475+
476+
const result = await CustomApiTool.fn()
477+
478+
expect(result.content[0].type).toBe('text')
479+
const parsedResult = JSON.parse(result.content[0].text)
480+
expect(parsedResult.metadata.fallback).toBe(true)
481+
expect(parsedResult.metadata.source).toBe('PWA_STOREFRONT_APP_PATH')
482+
})
483+
484+
test('should not use fallback when only some critical fields are missing', async () => {
485+
// Mock only 2 critical fields as null (should throw error, not use fallback)
486+
loadConfig.mockReturnValue({
487+
clientId: 'test-id',
488+
clientSecret: null,
489+
organizationId: null,
490+
instanceId: 'test-instance',
491+
shortCode: 'test',
492+
hostname: 'test.com'
493+
})
494+
495+
await expect(CustomApiTool.fn()).rejects.toThrow(
496+
'Required SFCC API credentials are missing'
497+
)
498+
})
499+
})
386500
})

0 commit comments

Comments
 (0)