@utaba/ucm-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +79 -0
- package/dist/clients/UcmApiClient.d.ts +53 -0
- package/dist/clients/UcmApiClient.js +297 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +68 -0
- package/dist/interfaces/ILogger.d.ts +8 -0
- package/dist/interfaces/ILogger.js +4 -0
- package/dist/interfaces/IMcpTool.d.ts +7 -0
- package/dist/interfaces/IMcpTool.js +3 -0
- package/dist/logging/ConsoleLogger.d.ts +16 -0
- package/dist/logging/ConsoleLogger.js +45 -0
- package/dist/logging/LoggerFactory.d.ts +9 -0
- package/dist/logging/LoggerFactory.js +12 -0
- package/dist/server/McpConfig.d.ts +26 -0
- package/dist/server/McpConfig.js +93 -0
- package/dist/server/McpHandler.d.ts +12 -0
- package/dist/server/McpHandler.js +69 -0
- package/dist/server/McpServer.d.ts +15 -0
- package/dist/server/McpServer.js +49 -0
- package/dist/server/ToolRegistry.d.ts +22 -0
- package/dist/server/ToolRegistry.js +85 -0
- package/dist/tools/artifacts/GetArtifactController.d.ts +34 -0
- package/dist/tools/artifacts/GetArtifactController.js +397 -0
- package/dist/tools/artifacts/GetLatestController.d.ts +39 -0
- package/dist/tools/artifacts/GetLatestController.js +469 -0
- package/dist/tools/artifacts/ListVersionsController.d.ts +43 -0
- package/dist/tools/artifacts/ListVersionsController.js +530 -0
- package/dist/tools/artifacts/PublishArtifactController.d.ts +37 -0
- package/dist/tools/artifacts/PublishArtifactController.js +605 -0
- package/dist/tools/base/BaseToolController.d.ts +16 -0
- package/dist/tools/base/BaseToolController.js +32 -0
- package/dist/tools/core/DeleteArtifactTool.d.ts +11 -0
- package/dist/tools/core/DeleteArtifactTool.js +82 -0
- package/dist/tools/core/GetArtifactTool.d.ts +13 -0
- package/dist/tools/core/GetArtifactTool.js +125 -0
- package/dist/tools/core/GetArtifactVersionsTool.d.ts +11 -0
- package/dist/tools/core/GetArtifactVersionsTool.js +63 -0
- package/dist/tools/core/GetChunkTool.d.ts +11 -0
- package/dist/tools/core/GetChunkTool.js +56 -0
- package/dist/tools/core/ListArtifactsTool.d.ts +11 -0
- package/dist/tools/core/ListArtifactsTool.js +84 -0
- package/dist/tools/core/PublishArtifactFromFileTool.d.ts +15 -0
- package/dist/tools/core/PublishArtifactFromFileTool.js +256 -0
- package/dist/tools/core/PublishArtifactTool.d.ts +13 -0
- package/dist/tools/core/PublishArtifactTool.js +197 -0
- package/dist/tools/discovery/BrowseCategoriesController.d.ts +25 -0
- package/dist/tools/discovery/BrowseCategoriesController.js +400 -0
- package/dist/tools/discovery/FindByPurposeController.d.ts +12 -0
- package/dist/tools/discovery/FindByPurposeController.js +131 -0
- package/dist/tools/discovery/ListAuthorsController.d.ts +20 -0
- package/dist/tools/discovery/ListAuthorsController.js +274 -0
- package/dist/tools/discovery/SearchArtifactsController.d.ts +14 -0
- package/dist/tools/discovery/SearchArtifactsController.js +226 -0
- package/dist/tools/list/ListNamespaceController.d.ts +1 -0
- package/dist/tools/list/ListNamespaceController.js +8 -0
- package/dist/tools/navigation/ExploreNamespaceController.d.ts +35 -0
- package/dist/tools/navigation/ExploreNamespaceController.js +548 -0
- package/dist/tools/utility/AuthorIndexTool.d.ts +11 -0
- package/dist/tools/utility/AuthorIndexTool.js +48 -0
- package/dist/tools/utility/HealthCheckController.d.ts +11 -0
- package/dist/tools/utility/HealthCheckController.js +56 -0
- package/dist/tools/utility/ListRepositoriesTool.d.ts +11 -0
- package/dist/tools/utility/ListRepositoriesTool.js +70 -0
- package/dist/tools/utility/QuickstartTool.d.ts +11 -0
- package/dist/tools/utility/QuickstartTool.js +36 -0
- package/dist/tools/utility/ValidatePathController.d.ts +30 -0
- package/dist/tools/utility/ValidatePathController.js +465 -0
- package/dist/types/UcmApiTypes.d.ts +40 -0
- package/dist/types/UcmApiTypes.js +4 -0
- package/dist/utils/McpErrorHandler.d.ts +25 -0
- package/dist/utils/McpErrorHandler.js +67 -0
- package/dist/utils/PathUtils.d.ts +61 -0
- package/dist/utils/PathUtils.js +178 -0
- package/dist/utils/ResponseChunker.d.ts +25 -0
- package/dist/utils/ResponseChunker.js +79 -0
- package/dist/utils/ValidationUtils.d.ts +10 -0
- package/dist/utils/ValidationUtils.js +50 -0
- package/package.json +37 -0
- package/package.json.backup +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Utaba AI
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# UCM MCP Server
|
|
2
|
+
|
|
3
|
+
Universal Context Manager (UCM) Model Context Protocol (MCP) server for AI-native package management.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Global installation (recommended)
|
|
9
|
+
npm install -g ucm-mcp-server
|
|
10
|
+
|
|
11
|
+
# Or using the scoped package
|
|
12
|
+
npm install -g @utaba/ucm-mcp-server
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Start the MCP server
|
|
19
|
+
ucm-mcp-server --ucm-url https://ucm.utaba.ai --auth-token YOUR_AUTH_TOKEN
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Get your auth token from https://ucm.utaba.ai
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
The UCM MCP server requires the following parameters:
|
|
27
|
+
|
|
28
|
+
- `--ucm-url <url>` (required): UCM API base URL
|
|
29
|
+
- `--auth-token <token>` (required): Authentication token in format `{authorid}:{apikey}`
|
|
30
|
+
|
|
31
|
+
## Available MCP Tools
|
|
32
|
+
|
|
33
|
+
The server provides 11 MCP tools following the `mcp_ucm_` naming convention:
|
|
34
|
+
|
|
35
|
+
### System Tools
|
|
36
|
+
- `mcp_ucm_health_check` - Check MCP server and UCM API connectivity
|
|
37
|
+
- `mcp_ucm_quickstart` - Get the UCM quickstart guide
|
|
38
|
+
|
|
39
|
+
### Discovery Tools
|
|
40
|
+
- `mcp_ucm_get_author_index` - Generate dynamic markdown index for an author
|
|
41
|
+
- `mcp_ucm_list_repositories` - List all repositories for an author
|
|
42
|
+
- `mcp_ucm_list_artifacts` - Browse the 4-level hierarchy
|
|
43
|
+
|
|
44
|
+
### Artifact Management
|
|
45
|
+
- `mcp_ucm_get_artifact` - Retrieve artifact content with auto-chunking
|
|
46
|
+
- `mcp_ucm_get_chunk` - Get chunks of large responses
|
|
47
|
+
- `mcp_ucm_publish_artifact` - Create/update artifacts
|
|
48
|
+
- `mcp_ucm_publish_artifact_fromfile` - File-based publishing (preferred for large files)
|
|
49
|
+
- `mcp_ucm_delete_artifact` - Remove artifacts
|
|
50
|
+
- `mcp_ucm_get_artifact_versions` - Get version history
|
|
51
|
+
|
|
52
|
+
## Usage with Claude Desktop
|
|
53
|
+
|
|
54
|
+
Add to your Claude Desktop configuration:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"ucm": {
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": [
|
|
62
|
+
"ucm-mcp-server",
|
|
63
|
+
"--ucm-url", "https://ucm.utaba.ai",
|
|
64
|
+
"--auth-token", "YOUR_AUTH_TOKEN"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
For detailed documentation, visit:
|
|
74
|
+
- UCM Platform: https://ucm.utaba.ai
|
|
75
|
+
- GitHub: https://github.com/utaba/ucm-mcp-server
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT - See LICENSE file for details
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ArtifactData, AuthorData } from '../types/UcmApiTypes.js';
|
|
2
|
+
export declare class UcmApiClient {
|
|
3
|
+
private baseUrl;
|
|
4
|
+
private authToken?;
|
|
5
|
+
private authorId?;
|
|
6
|
+
private client;
|
|
7
|
+
constructor(baseUrl: string, authToken?: string | undefined, timeout?: number, authorId?: string | undefined);
|
|
8
|
+
private setupInterceptors;
|
|
9
|
+
private ensureClient;
|
|
10
|
+
getAuthors(): Promise<AuthorData[]>;
|
|
11
|
+
getAuthor(authorId: string): Promise<AuthorData | null>;
|
|
12
|
+
private buildApiPath;
|
|
13
|
+
getArtifact(author?: string, repository?: string, category?: string, subcategory?: string, filename?: string, version?: string): Promise<ArtifactData>;
|
|
14
|
+
getLatestArtifact(author?: string, repository?: string, category?: string, subcategory?: string, filename?: string): Promise<ArtifactData>;
|
|
15
|
+
listArtifacts(author?: string, repository?: string, category?: string, subcategory?: string, offset?: number, limit?: number): Promise<{
|
|
16
|
+
data: any[];
|
|
17
|
+
pagination: {
|
|
18
|
+
offset: number;
|
|
19
|
+
limit: number;
|
|
20
|
+
total: number;
|
|
21
|
+
};
|
|
22
|
+
_links?: any;
|
|
23
|
+
}>;
|
|
24
|
+
searchArtifacts(filters?: {
|
|
25
|
+
repository?: string;
|
|
26
|
+
category?: string;
|
|
27
|
+
technology?: string;
|
|
28
|
+
subcategory?: string;
|
|
29
|
+
offset?: number;
|
|
30
|
+
limit?: number;
|
|
31
|
+
}): Promise<ArtifactData[]>;
|
|
32
|
+
publishArtifact(author: string, repository: string, category: string, subcategory: string, data: any): Promise<ArtifactData>;
|
|
33
|
+
updateArtifact(author: string, repository: string, category: string, subcategory: string, filename: string, version: string, data: any): Promise<ArtifactData>;
|
|
34
|
+
deleteArtifact(author: string, repository: string, category: string, subcategory: string, filename: string, version?: string): Promise<any>;
|
|
35
|
+
getArtifactVersions(author: string, repository: string, category: string, subcategory: string, filename: string): Promise<any>;
|
|
36
|
+
getCategories(): Promise<string[]>;
|
|
37
|
+
healthCheck(): Promise<{
|
|
38
|
+
status: string;
|
|
39
|
+
timestamp: string;
|
|
40
|
+
}>;
|
|
41
|
+
getQuickstart(): Promise<string>;
|
|
42
|
+
getAuthorIndex(author: string, repository?: string): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Cleanup method to properly dispose of HTTP client resources
|
|
45
|
+
* This helps prevent memory leaks from accumulated AbortSignal listeners
|
|
46
|
+
*/
|
|
47
|
+
cleanup(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Check if the client is still available for use
|
|
50
|
+
*/
|
|
51
|
+
isAvailable(): boolean;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=UcmApiClient.d.ts.map
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
export class UcmApiClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
authToken;
|
|
5
|
+
authorId;
|
|
6
|
+
client;
|
|
7
|
+
constructor(baseUrl, authToken, timeout = 600000, authorId) {
|
|
8
|
+
this.baseUrl = baseUrl;
|
|
9
|
+
this.authToken = authToken;
|
|
10
|
+
this.authorId = authorId;
|
|
11
|
+
this.client = axios.create({
|
|
12
|
+
baseURL: this.baseUrl,
|
|
13
|
+
timeout,
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
...(authToken && { 'Authorization': `Bearer ${authToken}` })
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
this.setupInterceptors();
|
|
20
|
+
}
|
|
21
|
+
setupInterceptors() {
|
|
22
|
+
this.client.interceptors.response.use((response) => response, (error) => {
|
|
23
|
+
// Extract error details from API response if available
|
|
24
|
+
const apiError = error.response?.data;
|
|
25
|
+
const status = error.response?.status;
|
|
26
|
+
const url = error.config?.url;
|
|
27
|
+
const errorMessage = apiError?.message || apiError?.error || error.message || JSON.stringify(apiError);
|
|
28
|
+
if (status === 404) {
|
|
29
|
+
return Promise.reject(new Error(`Resource not found: ${errorMessage}`));
|
|
30
|
+
}
|
|
31
|
+
if (status >= 500) {
|
|
32
|
+
// For 5xx errors, include API error details if available
|
|
33
|
+
return Promise.reject(new Error(`UCM API server error: ${errorMessage}`));
|
|
34
|
+
}
|
|
35
|
+
if (status >= 400 && status < 500) {
|
|
36
|
+
// For 4xx errors, extract validation details from API response
|
|
37
|
+
if (apiError?.message) {
|
|
38
|
+
return Promise.reject(new Error(apiError.message));
|
|
39
|
+
}
|
|
40
|
+
if (apiError?.error) {
|
|
41
|
+
return Promise.reject(new Error(apiError.error));
|
|
42
|
+
}
|
|
43
|
+
if (apiError?.errors && Array.isArray(apiError.errors)) {
|
|
44
|
+
return Promise.reject(new Error(apiError.errors.join(', ')));
|
|
45
|
+
}
|
|
46
|
+
// Fallback to JSON stringify the entire API error response
|
|
47
|
+
return Promise.reject(new Error(errorMessage));
|
|
48
|
+
}
|
|
49
|
+
// For other errors, preserve original error
|
|
50
|
+
return Promise.reject(error);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
ensureClient() {
|
|
54
|
+
if (!this.client) {
|
|
55
|
+
throw new Error('UcmApiClient has been cleaned up and is no longer available');
|
|
56
|
+
}
|
|
57
|
+
return this.client;
|
|
58
|
+
}
|
|
59
|
+
async getAuthors() {
|
|
60
|
+
const client = this.ensureClient();
|
|
61
|
+
const response = await client.get('/api/v1/authors');
|
|
62
|
+
return response.data;
|
|
63
|
+
}
|
|
64
|
+
async getAuthor(authorId) {
|
|
65
|
+
// No individual author endpoint - get from authors list
|
|
66
|
+
const authors = await this.getAuthors();
|
|
67
|
+
return authors.find(author => author.id === authorId) || null;
|
|
68
|
+
}
|
|
69
|
+
buildApiPath(api, author, repository, category, subcategory, filename, version) {
|
|
70
|
+
let path = `/api/v1/${api}`;
|
|
71
|
+
if (author) {
|
|
72
|
+
path += `/${author}`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
if (repository) {
|
|
78
|
+
path += `/${repository}`;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return path;
|
|
82
|
+
}
|
|
83
|
+
if (category) {
|
|
84
|
+
path += `/${category}`;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
return path;
|
|
88
|
+
}
|
|
89
|
+
if (subcategory) {
|
|
90
|
+
path += `/${subcategory}`;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return path;
|
|
94
|
+
}
|
|
95
|
+
if (filename) {
|
|
96
|
+
path += `/${filename}`;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
return path;
|
|
100
|
+
}
|
|
101
|
+
if (version) {
|
|
102
|
+
path += `@${version}`;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
return path;
|
|
106
|
+
}
|
|
107
|
+
return path;
|
|
108
|
+
}
|
|
109
|
+
async getArtifact(author, repository, category, subcategory, filename, version) {
|
|
110
|
+
// Default repository to 'main' for MVP
|
|
111
|
+
const repo = repository || 'main';
|
|
112
|
+
let metadataApiPath = this.buildApiPath('authors', author, repo, category, subcategory, filename, version);
|
|
113
|
+
// First get metadata from authors endpoint
|
|
114
|
+
const client = this.ensureClient();
|
|
115
|
+
const metadataResponse = await client.get(metadataApiPath);
|
|
116
|
+
const metadata = metadataResponse.data;
|
|
117
|
+
if (filename) {
|
|
118
|
+
let fileApiPath = this.buildApiPath('files', author, repo, category, subcategory, filename, version);
|
|
119
|
+
// Then get actual file content from files endpoint
|
|
120
|
+
let contentResponse = await client.get(fileApiPath, {
|
|
121
|
+
headers: { 'accept': metadataResponse.data }
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
...metadata.data,
|
|
125
|
+
content: contentResponse.data
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Combine metadata and content
|
|
129
|
+
return {
|
|
130
|
+
...metadata.data,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async getLatestArtifact(author, repository, category, subcategory, filename) {
|
|
134
|
+
// Default repository to 'main' for MVP
|
|
135
|
+
const repo = repository || 'main';
|
|
136
|
+
let metadataApiPath = this.buildApiPath('authors', author, repo, category, subcategory, filename);
|
|
137
|
+
const client = this.ensureClient();
|
|
138
|
+
const response = await client.get(metadataApiPath);
|
|
139
|
+
return response.data;
|
|
140
|
+
}
|
|
141
|
+
async listArtifacts(author, repository, category, subcategory, offset, limit) {
|
|
142
|
+
const params = new URLSearchParams();
|
|
143
|
+
if (offset !== undefined)
|
|
144
|
+
params.append('offset', offset.toString());
|
|
145
|
+
if (limit !== undefined)
|
|
146
|
+
params.append('limit', limit.toString());
|
|
147
|
+
const queryString = params.toString();
|
|
148
|
+
// Build URL based on provided parameters for exploratory browsing
|
|
149
|
+
// Default repository to 'main' for MVP
|
|
150
|
+
const repo = repository || 'main';
|
|
151
|
+
let metadataApiPath = this.buildApiPath('authors', author, repo, category, subcategory);
|
|
152
|
+
metadataApiPath += queryString ? `?${queryString}` : '';
|
|
153
|
+
const client = this.ensureClient();
|
|
154
|
+
const response = await client.get(metadataApiPath);
|
|
155
|
+
return response.data; // response.data contains the full structured response with pagination
|
|
156
|
+
}
|
|
157
|
+
async searchArtifacts(filters = {}) {
|
|
158
|
+
const params = new URLSearchParams();
|
|
159
|
+
// Default repository to 'main' for MVP if not specified
|
|
160
|
+
if (!filters.repository) {
|
|
161
|
+
filters.repository = 'main';
|
|
162
|
+
}
|
|
163
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
164
|
+
if (value !== undefined)
|
|
165
|
+
params.append(key, value.toString());
|
|
166
|
+
});
|
|
167
|
+
const client = this.ensureClient();
|
|
168
|
+
const response = await client.get(`/api/v1/artifacts?${params}`);
|
|
169
|
+
return response.data;
|
|
170
|
+
}
|
|
171
|
+
async publishArtifact(author, repository, category, subcategory, data) {
|
|
172
|
+
// Default repository to 'main' for MVP
|
|
173
|
+
const repo = repository || 'main';
|
|
174
|
+
// Build query parameters from the data object
|
|
175
|
+
const params = new URLSearchParams();
|
|
176
|
+
if (data.queryParams) {
|
|
177
|
+
// New API structure with query params
|
|
178
|
+
Object.entries(data.queryParams).forEach(([key, value]) => {
|
|
179
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
180
|
+
params.append(key, String(value));
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// Send raw content as text/plain body
|
|
184
|
+
const client = this.ensureClient();
|
|
185
|
+
const response = await client.post(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}?${params.toString()}`, data.content, {
|
|
186
|
+
headers: {
|
|
187
|
+
'Content-Type': 'text/plain'
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return response.data;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Legacy API structure (for backward compatibility)
|
|
194
|
+
const client = this.ensureClient();
|
|
195
|
+
const response = await client.post(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}`, data);
|
|
196
|
+
return response.data;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async updateArtifact(author, repository, category, subcategory, filename, version, data) {
|
|
200
|
+
// Default repository to 'main' for MVP
|
|
201
|
+
const repo = repository || 'main';
|
|
202
|
+
// Build query parameters from the data object
|
|
203
|
+
const params = new URLSearchParams();
|
|
204
|
+
if (data.queryParams) {
|
|
205
|
+
// New API structure with query params
|
|
206
|
+
Object.entries(data.queryParams).forEach(([key, value]) => {
|
|
207
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
208
|
+
params.append(key, String(value));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// Send raw content as text/plain body
|
|
212
|
+
const client = this.ensureClient();
|
|
213
|
+
const response = await client.put(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}@${version}?${params.toString()}`, data.content, {
|
|
214
|
+
headers: {
|
|
215
|
+
'Content-Type': 'text/plain'
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return response.data;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Legacy API structure (for backward compatibility)
|
|
222
|
+
const client = this.ensureClient();
|
|
223
|
+
const response = await client.put(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}@${version}`, data);
|
|
224
|
+
return response.data;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async deleteArtifact(author, repository, category, subcategory, filename, version) {
|
|
228
|
+
// Default repository to 'main' for MVP
|
|
229
|
+
const repo = repository || 'main';
|
|
230
|
+
const url = version
|
|
231
|
+
? `/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}@${version}`
|
|
232
|
+
: `/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}`;
|
|
233
|
+
const client = this.ensureClient();
|
|
234
|
+
const response = await client.delete(url);
|
|
235
|
+
return response.data;
|
|
236
|
+
}
|
|
237
|
+
async getArtifactVersions(author, repository, category, subcategory, filename) {
|
|
238
|
+
// Default repository to 'main' for MVP
|
|
239
|
+
const repo = repository || 'main';
|
|
240
|
+
const client = this.ensureClient();
|
|
241
|
+
const response = await client.get(`/api/v1/authors/${author}/${repo}/${category}/${subcategory}/${filename}/versions`);
|
|
242
|
+
return response.data;
|
|
243
|
+
}
|
|
244
|
+
async getCategories() {
|
|
245
|
+
// Categories are derived from artifacts since there's no dedicated endpoint
|
|
246
|
+
// Return the standard UCM categories
|
|
247
|
+
return ['commands', 'services', 'patterns', 'implementations', 'contracts', 'guidance', 'project'];
|
|
248
|
+
}
|
|
249
|
+
async healthCheck() {
|
|
250
|
+
const client = this.ensureClient();
|
|
251
|
+
const response = await client.get('/api/v1/health');
|
|
252
|
+
return response.data;
|
|
253
|
+
}
|
|
254
|
+
async getQuickstart() {
|
|
255
|
+
const client = this.ensureClient();
|
|
256
|
+
const response = await client.get('/api/v1/quickstart', {
|
|
257
|
+
headers: { 'accept': 'text/markdown' }
|
|
258
|
+
});
|
|
259
|
+
return response.data;
|
|
260
|
+
}
|
|
261
|
+
async getAuthorIndex(author, repository) {
|
|
262
|
+
const client = this.ensureClient();
|
|
263
|
+
let url;
|
|
264
|
+
if (repository) {
|
|
265
|
+
// Repository-specific index (future use)
|
|
266
|
+
url = `/api/v1/authors/${author}/${repository}/index`;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Author-level index
|
|
270
|
+
url = `/api/v1/authors/${author}/index`;
|
|
271
|
+
}
|
|
272
|
+
const response = await client.get(url, {
|
|
273
|
+
headers: { 'accept': 'text/markdown' }
|
|
274
|
+
});
|
|
275
|
+
return response.data;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Cleanup method to properly dispose of HTTP client resources
|
|
279
|
+
* This helps prevent memory leaks from accumulated AbortSignal listeners
|
|
280
|
+
*/
|
|
281
|
+
cleanup() {
|
|
282
|
+
if (this.client) {
|
|
283
|
+
// Clear interceptors to remove event listeners
|
|
284
|
+
this.client.interceptors.request.clear();
|
|
285
|
+
this.client.interceptors.response.clear();
|
|
286
|
+
// Set client to null to let GC handle cleanup
|
|
287
|
+
this.client = null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Check if the client is still available for use
|
|
292
|
+
*/
|
|
293
|
+
isAvailable() {
|
|
294
|
+
return this.client !== null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=UcmApiClient.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { McpServer } from './server/McpServer.js';
|
|
5
|
+
import { McpConfig } from './server/McpConfig.js';
|
|
6
|
+
import { LoggerFactory } from './logging/LoggerFactory.js';
|
|
7
|
+
// Increase max listeners to prevent AbortSignal memory leak warnings
|
|
8
|
+
// This is a safety net while we also implement proper cleanup
|
|
9
|
+
EventEmitter.defaultMaxListeners = 20;
|
|
10
|
+
async function main() {
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name('ucm-mcp-server')
|
|
14
|
+
.description('Universal Context Manager - Read the mcp_ucm_quickstart first to avoid mistakes')
|
|
15
|
+
.version('1.0.0')
|
|
16
|
+
.requiredOption('-u, --ucm-url <url>', 'UCM API base URL')
|
|
17
|
+
.option('-p, --port <port>', 'Server port', '3001')
|
|
18
|
+
.option('--log-level <level>', 'Log level', 'ERROR')
|
|
19
|
+
.option('--auth-token <token>', 'Authentication token for UCM API format is {auhtorid}:{apikey}. Get one from https://ucm.utaba.ai')
|
|
20
|
+
.option('--trusted-authors <authors>', 'Comma-separated list of trusted authors (e.g., "utaba,{anotherauthor}")', "utaba")
|
|
21
|
+
.parse();
|
|
22
|
+
const options = program.opts();
|
|
23
|
+
const config = new McpConfig({
|
|
24
|
+
ucmApiUrl: options.ucmUrl,
|
|
25
|
+
port: parseInt(options.port),
|
|
26
|
+
logLevel: options.logLevel,
|
|
27
|
+
authToken: options.authToken,
|
|
28
|
+
trustedAuthors: options.trustedAuthors ? options.trustedAuthors.split(',').map((a) => a.trim()) : ["utaba"]
|
|
29
|
+
});
|
|
30
|
+
// Set the log level in the factory before creating any loggers
|
|
31
|
+
LoggerFactory.setLogLevel(config.logLevel);
|
|
32
|
+
const logger = LoggerFactory.createLogger('MCP-Server');
|
|
33
|
+
const server = new McpServer(config, logger);
|
|
34
|
+
try {
|
|
35
|
+
await server.start();
|
|
36
|
+
logger.info('MCP-Server', 'UCM MCP Server started successfully');
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
logger.error('MCP-Server', 'Failed to start UCM MCP Server', '', error);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// Graceful shutdown handlers
|
|
43
|
+
const gracefulShutdown = async (signal) => {
|
|
44
|
+
logger.info('MCP-Server', `Received ${signal}, shutting down UCM MCP Server...`);
|
|
45
|
+
try {
|
|
46
|
+
await server.stop();
|
|
47
|
+
logger.info('MCP-Server', 'UCM MCP Server shutdown complete');
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
logger.error('MCP-Server', 'Error during shutdown', '', error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
56
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
57
|
+
// Handle uncaught exceptions and unhandled rejections
|
|
58
|
+
process.on('uncaughtException', (error) => {
|
|
59
|
+
logger.error('MCP-Server', 'Uncaught exception', '', error);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
63
|
+
logger.error('MCP-Server', 'Unhandled rejection', '', { reason, promise });
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
main().catch(console.error);
|
|
68
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ILogger {
|
|
2
|
+
debug(component: string, message: string, context?: string, metadata?: any): void;
|
|
3
|
+
info(component: string, message: string, context?: string, metadata?: any): void;
|
|
4
|
+
warn(component: string, message: string, context?: string, metadata?: any): void;
|
|
5
|
+
error(component: string, message: string, context?: string, metadata?: any): void;
|
|
6
|
+
logPerformance(component: string, operation: string, fileSize?: number, duration?: number, quota?: number): void;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=ILogger.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ILogger } from '../interfaces/ILogger.js';
|
|
2
|
+
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
|
3
|
+
export declare class ConsoleLogger implements ILogger {
|
|
4
|
+
private defaultComponent;
|
|
5
|
+
private logLevel;
|
|
6
|
+
private logLevelPriority;
|
|
7
|
+
constructor(defaultComponent: string, logLevel?: LogLevel);
|
|
8
|
+
private shouldLog;
|
|
9
|
+
debug(component: string, message: string, context?: string, metadata?: any): void;
|
|
10
|
+
info(component: string, message: string, context?: string, metadata?: any): void;
|
|
11
|
+
warn(component: string, message: string, context?: string, metadata?: any): void;
|
|
12
|
+
error(component: string, message: string, context?: string, metadata?: any): void;
|
|
13
|
+
logPerformance(component: string, operation: string, fileSize?: number, duration?: number, quota?: number): void;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=ConsoleLogger.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Source: Duplicated from ../core/logging/ConsoleLogger.ts for MCP Server independence
|
|
2
|
+
// Sync with Next.js app logger when updates are made
|
|
3
|
+
export class ConsoleLogger {
|
|
4
|
+
defaultComponent;
|
|
5
|
+
logLevel;
|
|
6
|
+
logLevelPriority = {
|
|
7
|
+
'DEBUG': 0,
|
|
8
|
+
'INFO': 1,
|
|
9
|
+
'WARN': 2,
|
|
10
|
+
'ERROR': 3
|
|
11
|
+
};
|
|
12
|
+
constructor(defaultComponent, logLevel = 'INFO') {
|
|
13
|
+
this.defaultComponent = defaultComponent;
|
|
14
|
+
this.logLevel = logLevel;
|
|
15
|
+
}
|
|
16
|
+
shouldLog(level) {
|
|
17
|
+
return this.logLevelPriority[level] >= this.logLevelPriority[this.logLevel];
|
|
18
|
+
}
|
|
19
|
+
debug(component, message, context, metadata) {
|
|
20
|
+
if (this.shouldLog('DEBUG')) {
|
|
21
|
+
console.debug(`[DEBUG] ${component}: ${message}`, metadata || '');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
info(component, message, context, metadata) {
|
|
25
|
+
if (this.shouldLog('INFO')) {
|
|
26
|
+
console.info(`[INFO] ${component}: ${message}`, metadata || '');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
warn(component, message, context, metadata) {
|
|
30
|
+
if (this.shouldLog('WARN')) {
|
|
31
|
+
console.warn(`[WARN] ${component}: ${message}`, metadata || '');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
error(component, message, context, metadata) {
|
|
35
|
+
if (this.shouldLog('ERROR')) {
|
|
36
|
+
console.error(`[ERROR] ${component}: ${message}`, metadata || '');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
logPerformance(component, operation, fileSize, duration, quota) {
|
|
40
|
+
if (this.shouldLog('INFO')) {
|
|
41
|
+
console.info(`[PERF] ${component}: ${operation} - Duration: ${duration}ms`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=ConsoleLogger.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ILogger } from '../interfaces/ILogger.js';
|
|
2
|
+
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
|
3
|
+
export declare class LoggerFactory {
|
|
4
|
+
private static logLevel;
|
|
5
|
+
static setLogLevel(level: LogLevel): void;
|
|
6
|
+
static createLogger(component: string): ILogger;
|
|
7
|
+
}
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=LoggerFactory.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// MCP Server-specific logger factory
|
|
2
|
+
import { ConsoleLogger } from './ConsoleLogger.js';
|
|
3
|
+
export class LoggerFactory {
|
|
4
|
+
static logLevel = 'INFO';
|
|
5
|
+
static setLogLevel(level) {
|
|
6
|
+
this.logLevel = level;
|
|
7
|
+
}
|
|
8
|
+
static createLogger(component) {
|
|
9
|
+
return new ConsoleLogger(component, this.logLevel);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=LoggerFactory.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface McpServerConfig {
|
|
2
|
+
ucmApiUrl: string;
|
|
3
|
+
port: number;
|
|
4
|
+
authToken?: string;
|
|
5
|
+
authorId?: string;
|
|
6
|
+
logLevel: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
|
7
|
+
requestTimeout: number;
|
|
8
|
+
trustedAuthors: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare class McpConfig {
|
|
11
|
+
private config;
|
|
12
|
+
constructor(options?: Partial<McpServerConfig>);
|
|
13
|
+
private extractAuthorIdFromToken;
|
|
14
|
+
private getEnvVar;
|
|
15
|
+
private parseTrustedAuthors;
|
|
16
|
+
private validateConfig;
|
|
17
|
+
get ucmApiUrl(): string;
|
|
18
|
+
get port(): number;
|
|
19
|
+
get authToken(): string | undefined;
|
|
20
|
+
get logLevel(): 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
|
21
|
+
get requestTimeout(): number;
|
|
22
|
+
get trustedAuthors(): string[];
|
|
23
|
+
get authorId(): string | undefined;
|
|
24
|
+
getFullConfig(): McpServerConfig;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=McpConfig.d.ts.map
|