UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI Productivity Platform

443 lines 18.7 kB
import axios from 'axios'; export class UcmLocalApiClient { baseUrl; authToken; authorId; organizationId; client; constructor(baseUrl, authToken, timeout = 600000, authorId, organizationId) { this.baseUrl = baseUrl; this.authToken = authToken; this.authorId = authorId; this.organizationId = organizationId; this.client = axios.create({ baseURL: this.baseUrl, timeout, headers: { 'Content-Type': 'application/json', ...(authToken && { 'Authorization': `Bearer ${authToken}` }) } }); this.setupInterceptors(); } /** * Get organizationId from constructor or throw error * For local MCP server, organizationId should be provided during initialization */ async getOrganizationId() { if (!this.organizationId) { throw new Error('Organization ID not configured. Please provide organizationId when initializing the MCP client.'); } return this.organizationId; } setupInterceptors() { this.client.interceptors.response.use((response) => response, (error) => { // Extract error details from API response if available const apiError = error.response?.data; const status = error.response?.status; const url = error.config?.url; const errorMessage = apiError?.message || apiError?.error || error.message || JSON.stringify(apiError); if (status === 404) { return Promise.reject(new Error(`Resource not found at ${url}: ${errorMessage}`)); } if (status >= 500) { // For 5xx errors, include API error details if available return Promise.reject(new Error(`UCM API server error: ${errorMessage}`)); } if (status >= 400 && status < 500) { // For 4xx errors, extract validation details from API response if (apiError?.message) { return Promise.reject(new Error(apiError.message)); } if (apiError?.error) { return Promise.reject(new Error(apiError.error)); } if (apiError?.errors && Array.isArray(apiError.errors)) { return Promise.reject(new Error(apiError.errors.join(', '))); } // Fallback to JSON stringify the entire API error response return Promise.reject(new Error(errorMessage)); } // For other errors, preserve original error return Promise.reject(error); }); } ensureClient() { if (!this.client) { throw new Error('UcmApiClient has been cleaned up and is no longer available'); } return this.client; } async getAuthors() { const client = this.ensureClient(); const response = await client.get('/api/v1/authors'); return response.data; } async getAuthor(authorId) { // No individual author endpoint - get from authors list const authors = await this.getAuthors(); return authors.find(author => author.id === authorId) || null; } buildApiPath(api, author, repository, category, subcategory, filename, version) { let path = `/api/v1/${api}`; if (author) { path += `/${author}`; } else { return path; } if (repository) { path += `/${repository}`; } else { return path; } if (category) { path += `/${category}`; } else { return path; } if (subcategory) { path += `/${subcategory}`; } else { return path; } if (filename) { path += `/${filename}`; } else { return path; } if (version) { path += `@${version}`; } else { return path; } return path; } async getArtifact(author, repository, category, subcategory, filename, version) { // Default repository to 'main' for MVP const repo = repository || 'main'; let metadataApiPath = this.buildApiPath('authors', author, repo, category, subcategory, filename, version); // First get metadata from authors endpoint const client = this.ensureClient(); const metadataResponse = await client.get(metadataApiPath); const metadata = metadataResponse.data; if (filename) { let fileApiPath = this.buildApiPath('files', author, repo, category, subcategory, filename, version); // Then get actual file content from files endpoint let contentResponse = await client.get(fileApiPath, { headers: { 'accept': metadataResponse.data } }); return { ...metadata.data, content: contentResponse.data }; } // Combine metadata and content return { ...metadata.data, }; } async getLatestArtifact(author, repository, category, subcategory, filename) { // Default repository to 'main' for MVP const repo = repository || 'main'; let metadataApiPath = this.buildApiPath('authors', author, repo, category, subcategory, filename); const client = this.ensureClient(); const response = await client.get(metadataApiPath); return response.data; } async listArtifacts(author, repository, category, subcategory, offset, limit) { const params = new URLSearchParams(); if (offset !== undefined) params.append('offset', offset.toString()); if (limit !== undefined) params.append('limit', limit.toString()); const queryString = params.toString(); // Build URL based on provided parameters for exploratory browsing // Default repository to 'main' for MVP const repo = repository; let metadataApiPath = this.buildApiPath('authors', author, repo, category, subcategory); metadataApiPath += queryString ? `?${queryString}` : ''; const client = this.ensureClient(); const response = await client.get(metadataApiPath); return response.data; // response.data contains the full structured response with pagination } async searchArtifacts(filters = {}) { const params = new URLSearchParams(); // Default repository to 'main' for MVP if not specified if (!filters.repository) { filters.repository = 'main'; } Object.entries(filters).forEach(([key, value]) => { if (value !== undefined) params.append(key, value.toString()); }); const client = this.ensureClient(); const response = await client.get(`/api/v1/artifacts?${params}`); return response.data; } async searchArtifactsByText(searchParams) { const params = new URLSearchParams(); // Add all parameters to URL Object.entries(searchParams).forEach(([key, value]) => { if (value !== undefined) params.append(key, value.toString()); }); const client = this.ensureClient(); const response = await client.get(`/api/v1/search/artifacts?${params}`); return response.data; } async publishArtifact(author, repository, category, subcategory, data) { // Default repository to 'main' for MVP const repo = repository || 'main'; // Build query parameters from the data object const params = new URLSearchParams(); if (data.queryParams) { // New API structure with query params Object.entries(data.queryParams).forEach(([key, value]) => { // Skip if value is undefined, null, empty string, or string "null"/"undefined" if (value !== undefined && value !== null && value !== '' && value !== 'null' && value !== 'undefined') { // Handle arrays (e.g., tags) by JSON stringifying them if (Array.isArray(value)) { params.append(key, JSON.stringify(value)); } else { params.append(key, String(value)); } } }); // Send raw content as text/plain body const client = this.ensureClient(); const response = await client.post(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}?${params.toString()}`, data.content, { headers: { 'Content-Type': 'text/plain' } }); return response.data; } else { // Legacy API structure (for backward compatibility) const client = this.ensureClient(); const response = await client.post(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}`, data); return response.data; } } async updateArtifact(author, repository, category, subcategory, filename, version, data) { // Default repository to 'main' for MVP const repo = repository || 'main'; // Build query parameters from the data object const params = new URLSearchParams(); if (data.queryParams) { // New API structure with query params Object.entries(data.queryParams).forEach(([key, value]) => { // Skip if value is undefined, null, empty string, or string "null"/"undefined" if (value !== undefined && value !== null && value !== '' && value !== 'null' && value !== 'undefined') { // Handle arrays (e.g., tags) by JSON stringifying them if (Array.isArray(value)) { params.append(key, JSON.stringify(value)); } else { params.append(key, String(value)); } } }); // Send raw content as text/plain body const client = this.ensureClient(); const response = await client.put(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}@${version}?${params.toString()}`, data.content, { headers: { 'Content-Type': 'text/plain' } }); return response.data; } else { // Legacy API structure (for backward compatibility) const client = this.ensureClient(); const response = await client.put(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}@${version}`, data); return response.data; } } async deleteArtifact(author, repository, category, subcategory, filename, version) { // Default repository to 'main' for MVP const repo = repository || 'main'; const url = version ? `/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}@${version}` : `/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}`; const client = this.ensureClient(); const response = await client.delete(url); return response.data; } async getArtifactVersions(author, repository, category, subcategory, filename) { // Default repository to 'main' for MVP const repo = repository || 'main'; const client = this.ensureClient(); const response = await client.get(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}/versions`); return response.data; } async getCategories() { // Categories are derived from artifacts since there's no dedicated endpoint // Return the standard UCM categories return ['commands', 'services', 'patterns', 'implementations', 'contracts', 'guidance', 'project']; } async healthCheck() { const client = this.ensureClient(); const response = await client.get('/api/v1/health'); return response.data; } async getQuickstart() { const client = this.ensureClient(); const response = await client.get('/api/v1/quickstart', { headers: { 'accept': 'text/markdown' } }); return response.data; } async getAuthorIndex(author, repository) { const client = this.ensureClient(); let url; if (repository) { // Repository-specific index (future use) url = `/api/v1/authors/${author}/${repository}/index`; } else { // Author-level index url = `/api/v1/authors/${author}/index`; } const response = await client.get(url, { headers: { 'accept': 'text/markdown' } }); return response.data; } async getAuthorRecents(author) { const client = this.ensureClient(); // Author-level recent activity (using new resources API structure) const url = `/api/v1/resources/author/${author}/recents`; const response = await client.get(url, { headers: { 'accept': 'text/markdown' } }); return response.data; } /** * Cleanup method to properly dispose of HTTP client resources * This helps prevent memory leaks from accumulated AbortSignal listeners */ cleanup() { if (this.client) { // Clear interceptors to remove event listeners this.client.interceptors.request.clear(); this.client.interceptors.response.clear(); // Set client to null to let GC handle cleanup this.client = null; } } /** * Check if the client is still available for use */ isAvailable() { return this.client !== null; } // Repository Management Methods async createRepository(author, data) { const client = this.ensureClient(); const response = await client.post(`/api/v1/authors/${author}`, data); return response.data; } async getRepository(author, repository) { const client = this.ensureClient(); const response = await client.get(`/api/v1/authors/${author}/${repository}`); return response.data; } async updateRepository(author, repository, data) { const client = this.ensureClient(); const response = await client.put(`/api/v1/authors/${author}/${repository}`, data); return response.data; } async deleteRepository(author, repository) { const client = this.ensureClient(); const response = await client.delete(`/api/v1/authors/${author}/${repository}`); return response.data; } async listRepositories(author, offset, limit) { const params = new URLSearchParams(); if (offset !== undefined) params.append('offset', offset.toString()); if (limit !== undefined) params.append('limit', limit.toString()); const queryString = params.toString(); const url = `/api/v1/authors/${author}${queryString ? `?${queryString}` : ''}`; const client = this.ensureClient(); const response = await client.get(url); return response.data; } async submitFeedback(data) { const client = this.ensureClient(); const response = await client.post('/api/v1/feedback', data); return response.data; } async editArtifactMetadata(author, repository, category, subcategory, filename, data) { const client = this.ensureClient(); const url = this.buildApiPath('authors', author, repository, category, subcategory, filename) + '/edit'; // Transform updateTags from comma-separated string to array for API const requestData = { ...data }; if (data.updateTags && typeof data.updateTags === 'string') { // Parse comma-separated tags into array, trim whitespace, filter empty strings requestData.updateTags = data.updateTags .split(',') .map(tag => tag.trim()) .filter(tag => tag.length > 0); } const response = await client.post(url, requestData); return response.data; } // SharePoint API methods /** * Generate markdown for SharePoint connections (safe, non-sensitive data only) */ async sharePointGenerateConnectionsMarkdown(organizationId, params) { const client = this.ensureClient(); const response = await client.post(`/api/org/${organizationId}/sharepoint/connections/markdown`, params); return response.data; } /** * List SharePoint connections for an organization (includes sensitive data - use with caution) */ async sharePointListConnections(organizationId, params) { const client = this.ensureClient(); const queryParams = new URLSearchParams(); if (params.limit) queryParams.set('limit', params.limit.toString()); if (params.offset) queryParams.set('offset', params.offset.toString()); const url = `/api/org/${organizationId}/sharepoint/connections${queryParams.toString() ? '?' + queryParams.toString() : ''}`; const response = await client.get(url); return response.data; } /** * Search SharePoint documents using Microsoft Search API */ async sharePointSearch(organizationId, connectionId, params) { const client = this.ensureClient(); const response = await client.post(`/api/org/${organizationId}/sharepoint/connections/${connectionId}/search`, params); return response.data; } /** * List folders and files in SharePoint with pagination */ async sharePointListFolders(organizationId, connectionId, params) { const client = this.ensureClient(); const response = await client.post(`/api/org/${organizationId}/sharepoint/connections/${connectionId}/folders`, params); return response.data; } /** * Read SharePoint file content with support for both fileUrl and legacy fileId */ async sharePointReadFile(organizationId, connectionId, params) { const client = this.ensureClient(); const response = await client.post(`/api/org/${organizationId}/sharepoint/connections/${connectionId}/read`, params); return response.data; } } //# sourceMappingURL=UcmApiClient.js.map