@xcud/remote-commander
Version:
MCP server for remote file operations via REST API
202 lines (201 loc) • 7.35 kB
JavaScript
import fetch from 'cross-fetch';
import { configManager } from './config-manager.js';
/**
* HTTP client for communicating with lit-server API
*/
export class HttpClient {
constructor() {
this.config = null;
}
/**
* Initialize HTTP client with configuration
*/
async init() {
const serverConfig = await configManager.getConfig();
if (!serverConfig.serverUrl || !serverConfig.basePath) {
throw new Error('HTTP client configuration incomplete. Please set serverUrl and basePath in config.');
}
this.config = {
serverUrl: serverConfig.serverUrl,
authUrl: serverConfig.authUrl,
authToken: serverConfig.authToken || '',
basePath: serverConfig.basePath,
username: serverConfig.username,
password: serverConfig.password
};
// If we have username/password but no auth token, try to get one
if (!this.config.authToken && this.config.username && this.config.password) {
await this.authenticate();
}
}
/**
* Authenticate with Keycloak to get bearer token
*/
async authenticate() {
if (!this.config || !this.config.username || !this.config.password) {
throw new Error('Username and password required for authentication');
}
try {
// Keycloak token endpoint - LIT realm with lit-api client
const authUrl = (await configManager.getConfig()).authUrl || 'http://localhost:8080';
const tokenUrl = `${authUrl}/realms/LIT/protocol/openid-connect/token`;
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'password',
client_id: 'lit-app',
username: this.config.username,
password: this.config.password,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Keycloak authentication failed: ${response.status} ${response.statusText} - ${errorText}`);
}
const tokenData = await response.json();
this.config.authToken = tokenData.access_token;
console.log('Successfully authenticated with Keycloak - token length:', this.config.authToken?.length);
}
catch (error) {
console.error('Keycloak authentication error:', error);
throw new Error(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Validate that path is within allowed basePath
*/
validatePath(requestPath) {
if (!this.config) {
throw new Error('HTTP client not initialized');
}
// For now, just ensure path starts with basePath
// TODO: Add more sophisticated path validation
if (!requestPath.startsWith(this.config.basePath)) {
return `${this.config.basePath}${requestPath.startsWith('/') ? '' : '/'}${requestPath}`;
}
return requestPath;
}
/**
* Make HTTP request to API
*/
async makeRequest(endpoint, data) {
if (!this.config) {
throw new Error('HTTP client not initialized');
}
const url = `${this.config.serverUrl}/api/commander/${endpoint}`;
console.log('Making request with token:', this.config.authToken ? 'Present' : 'Missing');
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.authToken}`
},
body: JSON.stringify(data)
});
if (!response.ok) {
return {
success: false,
error: `HTTP ${response.status}: ${response.statusText}`
};
}
const result = await response.json();
return {
success: true,
data: result
};
}
catch (error) {
return {
success: false,
error: `Network error: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Read file via API
*/
async readFile(request) {
const validatedPath = this.validatePath(request.path);
const response = await this.makeRequest('read_file', { ...request, path: validatedPath });
if (!response.success) {
return response;
}
// Extract the content from the API response
const content = response.data?.data || response.data;
return {
success: true,
data: content
};
}
/**
* Write file via API
*/
async writeFile(request) {
const validatedPath = this.validatePath(request.path);
return this.makeRequest('write_file', { ...request, path: validatedPath });
}
/**
* List directory via API
*/
async listDirectory(request) {
const validatedPath = this.validatePath(request.path);
const response = await this.makeRequest('list_directory', { ...request, path: validatedPath });
if (!response.success) {
return response;
}
// Convert the detailed response to simple string array
const items = response.data?.data || response.data || [];
const itemNames = items.map((item) => {
if (typeof item === 'string')
return item;
if (item.name)
return `[${item.type?.toUpperCase() || 'FILE'}] ${item.name}`;
return String(item);
});
return {
success: true,
data: itemNames
};
}
/**
* Search files via API
*/
async searchFiles(request) {
const validatedPath = this.validatePath(request.path);
return this.makeRequest('search_files', { ...request, path: validatedPath });
}
/**
* Search code via API
*/
async searchCode(request) {
const validatedPath = this.validatePath(request.path);
return this.makeRequest('search_code', { ...request, path: validatedPath });
}
/**
* Create directory via API
*/
async createDirectory(request) {
const validatedPath = this.validatePath(request.path);
return this.makeRequest('create_directory', { ...request, path: validatedPath });
}
/**
* Get file info via API
*/
async getFileInfo(request) {
const validatedPath = this.validatePath(request.path);
return this.makeRequest('get_file_info', { ...request, path: validatedPath });
}
/**
* Edit block via API
*/
async editBlock(request) {
const validatedPath = this.validatePath(request.file_path);
return this.makeRequest('edit_block', { ...request, file_path: validatedPath });
}
}
// Export singleton instance
export const httpClient = new HttpClient();