UNPKG

@utaba/ucm-mcp-server

Version:

Universal Context Manager MCP Server - AI Productivity Platform

460 lines 18.9 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(); } setupInterceptors() { this.client.interceptors.response.use((response) => response, (error) => { // For 4xx and 5xx errors, preserve the full error object so calling code // can access error.response.data.details (e.g., for loginUrl in auth errors) if (error.response && error.response.status >= 400) { // Pass through the full axios error - don't wrap it return Promise.reject(error); } // For other errors (network, timeout, etc.), 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 /** * List External Connections including SharePoint * Returns markdown with a table of connections available */ async listExternalConnections(params) { const client = this.ensureClient(); const url = `/api/v1/connections`; const response = await client.get(url, { params }); return response.data; } /** * Search SharePoint documents using Microsoft Search API * Uses V1 API - organizationId is auto-detected from auth token */ async sharePointSearch(connectionId, params) { const client = this.ensureClient(); const response = await client.post(`/api/v1/connections/sharepoint/search`, { connectionId, ...params }); return response.data; } /** * List folders and files in SharePoint with pagination * Uses V1 API - organizationId is auto-detected from auth token */ async sharePointListFolders(connectionId, params) { const client = this.ensureClient(); const response = await client.post(`/api/v1/connections/sharepoint/folders`, { connectionId, ...params }); return response.data; } /** * Read SharePoint file content with support for both fileUrl and legacy fileId * Uses V1 API - organizationId is auto-detected from auth token */ async sharePointReadFile(connectionId, params) { const client = this.ensureClient(); const response = await client.post(`/api/v1/connections/sharepoint/read`, { connectionId, ...params }); return response.data; } /** * Read SharePoint related file (image/embedded file extracted from document) * Uses V1 API - organizationId is auto-detected from auth token * Accepts full URI from markdown (with or without protocol prefix) * Returns complete file content (no pagination support) */ async sharePointReadRelatedFile(uri) { const client = this.ensureClient(); const response = await client.post(`/api/v1/connections/sharepoint/related-file`, { uri }, // Pass URI in JSON body { responseType: 'arraybuffer' // Binary data }); // Convert binary response to base64 for MCP transport const base64Content = Buffer.from(response.data).toString('base64'); const contentType = response.headers['content-type'] || 'application/octet-stream'; const contentLength = parseInt(response.headers['content-length'] || '0', 10); // Extract fileName from URI for response metadata const fileName = this.extractFileNameFromUri(uri); return { fileName, mimeType: contentType, size: contentLength, content: base64Content }; } /** * Extract fileName from SharePoint related file URI * Helper method for metadata purposes */ extractFileNameFromUri(uri) { // Strip protocol prefix if present const cleaned = uri.replace(/^ucm_sharepoint_read_relatedfile:\/\//, ''); // Extract path portion before query string const pathPart = cleaned.split('?')[0]; // URL-decode the fileName return decodeURIComponent(pathPart); } /** * Revoke SharePoint OnBehalfOf authorization for a connection * Deletes user's access tokens from Key Vault and database * Uses V1 API - organizationId is auto-detected from auth token */ async sharePointSignOut(connectionId) { const client = this.ensureClient(); const response = await client.post(`/api/v1/connections/sharepoint/signout`, { connectionId }); return response.data; } } //# sourceMappingURL=UcmLocalApiClient.js.map