UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI-native artifact management

373 lines 15.3 kB
import axios from 'axios'; export class UcmApiClient { baseUrl; authToken; authorId; client; constructor(baseUrl, authToken, timeout = 600000, authorId) { this.baseUrl = baseUrl; this.authToken = authToken; this.authorId = authorId; this.client = axios.create({ baseURL: this.baseUrl, timeout, headers: { 'Content-Type': 'application/json', ...(authToken && { 'Authorization': `Bearer ${authToken}` }) } }); this.setupInterceptors(); } 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]) => { if (value !== undefined && value !== null && value !== '') { // 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]) => { if (value !== undefined && value !== null && value !== '') { // 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'; const response = await client.post(url, data); return response.data; } } //# sourceMappingURL=UcmApiClient.js.map