@utaba/ucm-mcp-server
Version:
Universal Context Manager MCP Server - AI-native artifact management
373 lines • 15.3 kB
JavaScript
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