Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,18 @@ sfcc-dev-mcp/
│ │ ├── server.ts # Main MCP server implementation
│ │ └── tool-definitions.ts # MCP tool schema definitions
│ ├── clients/ # API clients for different services
│ │ ├── base/ # Base client classes and shared functionality
│ │ │ ├── http-client.ts # Base HTTP client with authentication
│ │ │ ├── ocapi-auth-client.ts # OCAPI OAuth authentication client
│ │ │ └── oauth-token.ts # OAuth token management for OCAPI
│ │ ├── ocapi/ # Specialized OCAPI clients
│ │ │ ├── catalog-client.ts # OCAPI catalog operations
│ │ │ ├── site-preferences-client.ts # Site preferences management
│ │ │ └── system-objects-client.ts # System object definitions
│ │ ├── log-client.ts # SFCC log analysis client
│ │ ├── docs-client.ts # SFCC documentation client
│ │ ├── ocapi-client.ts # OCAPI client for system objects
│ │ ├── ocapi-client.ts # Main OCAPI client coordinator
│ │ └── best-practices-client.ts # Best practices guide client
│ ├── auth/ # Authentication and OAuth management
│ │ └── oauth-token.ts # OAuth token management
│ ├── config/ # Configuration management
│ │ ├── config.ts # Configuration loading and validation
│ │ ├── configuration-factory.ts # Config factory for different modes
Expand All @@ -67,6 +73,8 @@ sfcc-dev-mcp/
│ │ ├── cache.ts # Caching layer for API responses
│ │ ├── logger.ts # Structured logging system
│ │ ├── utils.ts # Common utility functions
│ │ ├── validator.ts # Input validation utilities
│ │ ├── query-builder.ts # Query string building utilities
│ │ └── path-resolver.ts # File path resolution utilities
│ └── types/ # TypeScript type definitions
│ └── types.ts # Comprehensive type definitions
Expand Down Expand Up @@ -98,16 +106,22 @@ sfcc-dev-mcp/
- Provides error handling and response formatting

#### **Client Architecture**
- **DocsClient** (`clients/docs-client.ts`): Processes SFCC documentation and provides search capabilities
- **LogClient** (`clients/log-client.ts`): Connects to SFCC instances for log analysis and monitoring
- **OCAPIClient** (`clients/ocapi-client.ts`): Interfaces with SFCC OCAPI for system object data
- **BestPracticesClient** (`clients/best-practices-client.ts`): Serves curated development guides and references

#### **Authentication & Security** (`auth/`)
- **OAuth Token Management** (`oauth-token.ts`): Handles SFCC OAuth flows with automatic renewal for local development
- **Credential Security**: Secure local storage and handling of SFCC instance credentials
- **Local Protection**: Prevents accidental credential exposure and limits network access to localhost
- **Rate Limiting**: Implements reasonable rate limiting to avoid overwhelming developer sandbox instances
##### **Base Client Infrastructure** (`clients/base/`)
- **BaseHttpClient** (`http-client.ts`): Abstract base class providing HTTP operations, authentication handling, and error recovery
- **OCAPIAuthClient** (`ocapi-auth-client.ts`): OCAPI-specific OAuth authentication with token management and automatic renewal
- **TokenManager** (`oauth-token.ts`): Singleton OAuth token manager for SFCC OCAPI authentication with automatic expiration handling

##### **Specialized OCAPI Clients** (`clients/ocapi/`)
- **OCAPICatalogClient** (`catalog-client.ts`): Handles catalog operations, product searches, and category management
- **OCAPISitePreferencesClient** (`site-preferences-client.ts`): Manages site preference searches and configuration discovery
- **OCAPISystemObjectsClient** (`system-objects-client.ts`): Provides system object definitions, attribute schemas, and custom object exploration

##### **Service Clients** (`clients/`)
- **DocsClient** (`docs-client.ts`): Processes SFCC documentation and provides search capabilities across all namespaces
- **LogClient** (`log-client.ts`): Connects to SFCC instances for real-time log analysis and monitoring
- **OCAPIClient** (`ocapi-client.ts`): Main OCAPI coordinator that orchestrates specialized clients and provides unified interface
- **BestPracticesClient** (`best-practices-client.ts`): Serves curated development guides, security recommendations, and hook references

#### **Configuration Management** (`config/`)
- **Configuration Factory** (`configuration-factory.ts`): Creates configurations for different modes
Expand Down Expand Up @@ -234,7 +248,7 @@ When working on this project:

- **Adding New Tools**: Define schema in `core/tool-definitions.ts`, implement handler in appropriate client in `clients/`
- **Updating Documentation**: Modify files in `docs/` and run conversion scripts
- **Enhancing Authentication**: Update `auth/oauth-token.ts` and client authentication logic
- **Enhancing Authentication**: Update `clients/base/oauth-token.ts` and client authentication logic
- **Improving Caching**: Enhance `utils/cache.ts` for better performance and data freshness
- **Adding Configuration Options**: Update `config/` modules for new configuration capabilities
- **Adding Tests**: Create comprehensive test coverage in the `tests/` directory
Expand Down
152 changes: 152 additions & 0 deletions src/clients/base/http-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Base HTTP Client for SFCC API requests
*
* This module provides a foundation for making authenticated HTTP requests to SFCC APIs.
* It handles common concerns like authentication, request/response formatting, and error handling.
*/

import { Logger } from '../../utils/logger.js';

/**
* HTTP request options interface
*/
export interface HttpRequestOptions extends RequestInit {
headers?: Record<string, string>;
}

/**
* Base HTTP client for SFCC API communication
*/
export abstract class BaseHttpClient {
protected baseUrl: string;
protected logger: Logger;

constructor(baseUrl: string, loggerContext: string) {
this.baseUrl = baseUrl;
this.logger = new Logger(loggerContext);
}

/**
* Get authentication headers - must be implemented by subclasses
*/
protected abstract getAuthHeaders(): Promise<Record<string, string>>;

/**
* Handle authentication errors - can be overridden by subclasses
*/
protected async handleAuthError(): Promise<void> {
// Default implementation does nothing
// Subclasses can override to clear tokens, retry, etc.
}

/**
* Make an authenticated HTTP request
*/
protected async makeRequest<T>(
endpoint: string,
options: HttpRequestOptions = {},
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const method = options.method ?? 'GET';
Copy link

Copilot AI Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider extracting the method assignment outside the log statement to improve readability and make debugging easier.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed


this.logger.debug(`Making ${method} request to: ${endpoint}`);

// Get authentication headers
const authHeaders = await this.getAuthHeaders();

const requestOptions: RequestInit = {
...options,
headers: {
'Content-Type': 'application/json',
...authHeaders,
...options.headers,
},
};

try {
const response = await fetch(url, requestOptions);

if (!response.ok) {
// Handle authentication errors
if (response.status === 401) {
this.logger.debug('Received 401, attempting to handle auth error');
await this.handleAuthError();

// Retry with fresh authentication
const newAuthHeaders = await this.getAuthHeaders();
requestOptions.headers = {
...requestOptions.headers,
...newAuthHeaders,
};

const retryResponse = await fetch(url, requestOptions);
if (!retryResponse.ok) {
const errorText = await retryResponse.text();
throw new Error(
`Request failed after retry: ${retryResponse.status} ${retryResponse.statusText} - ${errorText}`,
);
}

this.logger.debug('Retry request successful');
return retryResponse.json();
}

const errorText = await response.text();
throw new Error(`Request failed: ${response.status} ${response.statusText} - ${errorText}`);
}

this.logger.debug(`Request to ${endpoint} completed successfully`);
return response.json();
} catch (error) {
this.logger.error(`Network error during request to ${endpoint}: ${error}`);
throw error;
}
}

/**
* GET request
*/
protected async get<T>(endpoint: string): Promise<T> {
return this.makeRequest<T>(endpoint, { method: 'GET' });
}

/**
* POST request
*/
protected async post<T>(endpoint: string, data?: any): Promise<T> {
const options: HttpRequestOptions = { method: 'POST' };
if (data) {
options.body = JSON.stringify(data);
}
return this.makeRequest<T>(endpoint, options);
}

/**
* PUT request
*/
protected async put<T>(endpoint: string, data?: any): Promise<T> {
const options: HttpRequestOptions = { method: 'PUT' };
if (data) {
options.body = JSON.stringify(data);
}
return this.makeRequest<T>(endpoint, options);
}

/**
* PATCH request
*/
protected async patch<T>(endpoint: string, data?: any): Promise<T> {
const options: HttpRequestOptions = { method: 'PATCH' };
if (data) {
options.body = JSON.stringify(data);
}
return this.makeRequest<T>(endpoint, options);
}

/**
* DELETE request
*/
protected async delete<T>(endpoint: string): Promise<T> {
return this.makeRequest<T>(endpoint, { method: 'DELETE' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* for SFCC OCAPI requests. It handles automatic token refresh when tokens expire.
*/

import { OAuthToken, OAuthTokenResponse } from '../types/types.js';
import { OAuthToken, OAuthTokenResponse } from '../../types/types.js';

/**
* Singleton class for managing OAuth tokens
Expand Down
120 changes: 120 additions & 0 deletions src/clients/base/ocapi-auth-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* OCAPI Authentication Client
*
* This module handles OAuth 2.0 authentication specifically for SFCC OCAPI requests.
* It extends the base HTTP client with OCAPI-specific authentication logic.
*/

import { OCAPIConfig, OAuthTokenResponse } from '../../types/types.js';
import { TokenManager } from './oauth-token.js';
import { BaseHttpClient } from './http-client.js';

// OCAPI authentication constants
const OCAPI_AUTH_CONSTANTS = {
AUTH_URL: 'https://account.demandware.com/dwsso/oauth2/access_token',
GRANT_TYPE: 'client_credentials',
FORM_CONTENT_TYPE: 'application/x-www-form-urlencoded',
} as const;

/**
* OCAPI Authentication Client
* Handles OAuth 2.0 Client Credentials flow for OCAPI access
*/
export class OCAPIAuthClient extends BaseHttpClient {
private config: OCAPIConfig;
private tokenManager: TokenManager;

constructor(config: OCAPIConfig) {
super('', 'OCAPIAuthClient'); // Initialize BaseHttpClient with logger
Copy link

Copilot AI Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing an empty string as baseUrl and then overriding it in subclasses is confusing. Consider redesigning the base class to accept optional baseUrl or provide a proper initialization pattern.

Suggested change
super('', 'OCAPIAuthClient'); // Initialize BaseHttpClient with logger
super(OCAPI_AUTH_CONSTANTS.AUTH_URL, 'OCAPIAuthClient'); // Initialize BaseHttpClient with logger and baseUrl

Copilot uses AI. Check for mistakes.
this.config = config;
this.tokenManager = TokenManager.getInstance();
}

/**
* Get authentication headers for OCAPI requests
*/
protected async getAuthHeaders(): Promise<Record<string, string>> {
const accessToken = await this.getAccessToken();
return {
'Authorization': `Bearer ${accessToken}`,
};
}

/**
* Handle authentication errors by clearing the stored token
*/
protected async handleAuthError(): Promise<void> {
this.logger.debug('Clearing token due to authentication error');
this.tokenManager.clearToken(this.config.hostname, this.config.clientId);
}

/**
* Get a valid OAuth access token
*/
private async getAccessToken(): Promise<string> {
this.logger.debug('Attempting to get access token');

// Check if we have a valid token first
const existingToken = this.tokenManager.getValidToken(this.config.hostname, this.config.clientId);
if (existingToken) {
this.logger.debug('Using existing valid token');
return existingToken;
}

this.logger.debug('No valid token found, requesting new token');
return this.requestNewToken();
}

/**
* Request a new OAuth token from SFCC
*/
private async requestNewToken(): Promise<string> {
// Create Basic Auth header using client credentials
const credentials = `${this.config.clientId}:${this.config.clientSecret}`;
const encodedCredentials = Buffer.from(credentials).toString('base64');

try {
const response = await fetch(OCAPI_AUTH_CONSTANTS.AUTH_URL, {
method: 'POST',
headers: {
'Authorization': `Basic ${encodedCredentials}`,
'Content-Type': OCAPI_AUTH_CONSTANTS.FORM_CONTENT_TYPE,
},
body: `grant_type=${OCAPI_AUTH_CONSTANTS.GRANT_TYPE}`,
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`OAuth authentication failed: ${response.status} ${response.statusText} - ${errorText}`);
}

const tokenResponse: OAuthTokenResponse = await response.json();
this.logger.debug('Successfully obtained new access token');

// Store the token for future use
this.tokenManager.storeToken(this.config.hostname, this.config.clientId, tokenResponse);

return tokenResponse.access_token;
} catch (error) {
this.logger.error(`Failed to get access token: ${error}`);
throw new Error(`Failed to get access token: ${error}`);
}
}

/**
* Get current token expiration for debugging
*/
getTokenExpiration(): Date | null {
return this.tokenManager.getTokenExpiration(this.config.hostname, this.config.clientId);
}

/**
* Force refresh the token
*/
async refreshToken(): Promise<void> {
this.logger.debug('Forcing token refresh');
this.tokenManager.clearToken(this.config.hostname, this.config.clientId);
await this.getAccessToken();
this.logger.debug('Token refresh completed');
}
}
Loading