@utaba/ucm-mcp-server
Version:
Universal Context Manager MCP Server - AI Productivity Platform
460 lines • 18.9 kB
JavaScript
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