devops-mcp-server-extension 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 +218 -0
- package/README.md +527 -0
- package/README.zh-cn.md +503 -0
- package/dist/common/errors.js +77 -0
- package/dist/common/modularTemplates.js +483 -0
- package/dist/common/pipelineTemplates.js +19 -0
- package/dist/common/toolsetManager.js +123 -0
- package/dist/common/toolsets.js +23 -0
- package/dist/common/types.js +60 -0
- package/dist/common/utils.js +381 -0
- package/dist/common/version.js +1 -0
- package/dist/index.js +225 -0
- package/dist/operations/appstack/appOrchestrations.js +260 -0
- package/dist/operations/appstack/appTags.js +168 -0
- package/dist/operations/appstack/appTemplates.js +72 -0
- package/dist/operations/appstack/applications.js +171 -0
- package/dist/operations/appstack/changeOrders.js +320 -0
- package/dist/operations/appstack/changeRequests.js +288 -0
- package/dist/operations/appstack/deploymentResources.js +286 -0
- package/dist/operations/appstack/globalVars.js +221 -0
- package/dist/operations/appstack/releaseWorkflows.js +695 -0
- package/dist/operations/appstack/variableGroups.js +245 -0
- package/dist/operations/codeup/branches.js +157 -0
- package/dist/operations/codeup/changeRequestComments.js +140 -0
- package/dist/operations/codeup/changeRequests.js +230 -0
- package/dist/operations/codeup/commits.js +121 -0
- package/dist/operations/codeup/compare.js +30 -0
- package/dist/operations/codeup/files.js +249 -0
- package/dist/operations/codeup/repositories.js +71 -0
- package/dist/operations/codeup/types.js +414 -0
- package/dist/operations/flow/hostGroup.js +52 -0
- package/dist/operations/flow/pipeline.js +609 -0
- package/dist/operations/flow/pipelineJob.js +126 -0
- package/dist/operations/flow/resourceMember.js +137 -0
- package/dist/operations/flow/serviceConnection.js +27 -0
- package/dist/operations/flow/tag.js +191 -0
- package/dist/operations/flow/types.js +523 -0
- package/dist/operations/flow/vmDeployOrder.js +171 -0
- package/dist/operations/organization/members.js +106 -0
- package/dist/operations/organization/organization.js +110 -0
- package/dist/operations/organization/types.js +111 -0
- package/dist/operations/packages/artifacts.js +71 -0
- package/dist/operations/packages/repositories.js +39 -0
- package/dist/operations/packages/types.js +56 -0
- package/dist/operations/projex/effort.js +122 -0
- package/dist/operations/projex/project.js +243 -0
- package/dist/operations/projex/sprint.js +103 -0
- package/dist/operations/projex/types.js +618 -0
- package/dist/operations/projex/workitem.js +826 -0
- package/dist/operations/testhub/testcases.js +240 -0
- package/dist/operations/testhub/testplans.js +128 -0
- package/dist/tool-handlers/appstack-app-release-workflows.js +103 -0
- package/dist/tool-handlers/appstack-change-orders.js +55 -0
- package/dist/tool-handlers/appstack-change-requests.js +49 -0
- package/dist/tool-handlers/appstack-deployment-resources.js +31 -0
- package/dist/tool-handlers/appstack-global-vars.js +37 -0
- package/dist/tool-handlers/appstack-orchestrations.js +49 -0
- package/dist/tool-handlers/appstack-release-workflows.js +37 -0
- package/dist/tool-handlers/appstack-tags.js +37 -0
- package/dist/tool-handlers/appstack-templates.js +19 -0
- package/dist/tool-handlers/appstack-variable-groups.js +55 -0
- package/dist/tool-handlers/appstack.js +37 -0
- package/dist/tool-handlers/base.js +25 -0
- package/dist/tool-handlers/code-management.js +150 -0
- package/dist/tool-handlers/commit.js +31 -0
- package/dist/tool-handlers/effort.js +103 -0
- package/dist/tool-handlers/index.js +119 -0
- package/dist/tool-handlers/organization.js +72 -0
- package/dist/tool-handlers/packages.js +32 -0
- package/dist/tool-handlers/pipeline.js +289 -0
- package/dist/tool-handlers/project-management.js +201 -0
- package/dist/tool-handlers/resourceMember.js +43 -0
- package/dist/tool-handlers/service-connections.js +16 -0
- package/dist/tool-handlers/tag.js +64 -0
- package/dist/tool-handlers/test-management.js +74 -0
- package/dist/tool-handlers/vmDeployOrder.js +50 -0
- package/dist/tool-registry/appstack-app-release-workflows.js +80 -0
- package/dist/tool-registry/appstack-change-orders.js +40 -0
- package/dist/tool-registry/appstack-change-requests.js +35 -0
- package/dist/tool-registry/appstack-deployment-resources.js +20 -0
- package/dist/tool-registry/appstack-global-vars.js +25 -0
- package/dist/tool-registry/appstack-orchestrations.js +35 -0
- package/dist/tool-registry/appstack-release-workflows.js +25 -0
- package/dist/tool-registry/appstack-tags.js +25 -0
- package/dist/tool-registry/appstack-templates.js +10 -0
- package/dist/tool-registry/appstack-variable-groups.js +40 -0
- package/dist/tool-registry/appstack.js +25 -0
- package/dist/tool-registry/base.js +19 -0
- package/dist/tool-registry/code-management.js +109 -0
- package/dist/tool-registry/commit.js +20 -0
- package/dist/tool-registry/effort.js +39 -0
- package/dist/tool-registry/index.js +7 -0
- package/dist/tool-registry/organization.js +65 -0
- package/dist/tool-registry/packages.js +21 -0
- package/dist/tool-registry/pipeline.js +190 -0
- package/dist/tool-registry/project-management.js +143 -0
- package/dist/tool-registry/resourceMember.js +29 -0
- package/dist/tool-registry/service-connections.js +10 -0
- package/dist/tool-registry/tag.js +44 -0
- package/dist/tool-registry/test-management.js +59 -0
- package/dist/tool-registry/vmDeployOrder.js +34 -0
- package/package.json +52 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Organization types
|
|
2
|
+
export { UserInfoSchema, CurrentUserSchema, GetOrganizationMembersSchema, OrganizationRoleSchema, OrganizationRole, ListOrganizationRolesSchema, GetOrganizationRoleSchema, GetOrganizationDepartmentAncestorsSchema, GetOrganizationDepartmentInfoSchema, GetOrganizationDepartmentsSchema, CurrentOrganizationInfoSchema, OrganizationInfoSchema, UserOrganizationsInfoSchema, DepartmentInfoSchema, OrganizationDepartmentsSchema, MemberInfoSchema, OrganizationMembersSchema, GetOrganizationMemberInfoSchema, GetOrganizationMemberByUserIdInfoSchema, SearchOrganizationMembersSchema, SearchOrganizationMembersResultSchema } from "../operations/organization/types.js";
|
|
3
|
+
// Codeup types
|
|
4
|
+
export {
|
|
5
|
+
// Branch schemas
|
|
6
|
+
CreateBranchSchema, GetBranchSchema, DeleteBranchSchema, ListBranchesSchema,
|
|
7
|
+
// File schemas
|
|
8
|
+
GetFileBlobsSchema, CreateFileSchema, UpdateFileSchema, DeleteFileSchema, ListFilesSchema,
|
|
9
|
+
// Repository schemas
|
|
10
|
+
GetRepositorySchema, ListRepositoriesSchema,
|
|
11
|
+
// Compare schemas
|
|
12
|
+
GetCompareSchema,
|
|
13
|
+
// Change request schemas
|
|
14
|
+
GetChangeRequestSchema, ListChangeRequestsSchema, CreateChangeRequestSchema, ListChangeRequestPatchSetsSchema,
|
|
15
|
+
// Change request comment schemas
|
|
16
|
+
CreateChangeRequestCommentSchema, ListChangeRequestCommentsSchema, UpdateChangeRequestCommentSchema,
|
|
17
|
+
// Commit schemas
|
|
18
|
+
ListCommitsRequestSchema, GetCommitRequestSchema, CreateCommitCommentRequestSchema, DevopsCommitVOSchema, DevopsCommitStatVOSchema, CreateCommitCommentVOSchema } from "../operations/codeup/types.js";
|
|
19
|
+
// Projex types
|
|
20
|
+
export {
|
|
21
|
+
// Project schemas
|
|
22
|
+
GetProjectSchema, SearchProjectsSchema, ListProjectMembersSchema,
|
|
23
|
+
// Sprint schemas
|
|
24
|
+
GetSprintSchema, ListSprintsSchema, CreateSprintSchema, UpdateSprintSchema,
|
|
25
|
+
// Work item schemas
|
|
26
|
+
GetWorkItemSchema, CreateWorkItemSchema, SearchWorkitemsSchema, UpdateWorkItemSchema,
|
|
27
|
+
// Work item type schemas
|
|
28
|
+
ListAllWorkItemTypesSchema, ListWorkItemTypesSchema, GetWorkItemTypeSchema, ListWorkItemRelationWorkItemTypesSchema, GetWorkItemTypeFieldConfigSchema, GetWorkItemWorkflowSchema,
|
|
29
|
+
// Work item comment schemas
|
|
30
|
+
ListWorkItemCommentsSchema, CreateWorkItemCommentSchema,
|
|
31
|
+
// Work item activity schemas
|
|
32
|
+
ListWorkitemActivitiesSchema,
|
|
33
|
+
// Work item attachment schemas
|
|
34
|
+
ListWorkitemAttachmentsSchema,
|
|
35
|
+
// Work item file schemas
|
|
36
|
+
GetWorkitemFileSchema,
|
|
37
|
+
// Work item relation record schemas
|
|
38
|
+
ListWorkitemRelationRecordsSchema, CreateWorkitemRelationRecordSchema, DeleteWorkitemRelationRecordSchema,
|
|
39
|
+
// Effort schemas
|
|
40
|
+
ListCurrentUserEffortRecordsSchema, ListEffortRecordsSchema, CreateEffortRecordSchema, ListEstimatedEffortsSchema, CreateEstimatedEffortSchema, UpdateEffortRecordSchema, UpdateEstimatedEffortSchema } from "../operations/projex/types.js";
|
|
41
|
+
// Flow types
|
|
42
|
+
export {
|
|
43
|
+
// Pipeline schemas
|
|
44
|
+
GetPipelineSchema, ListPipelinesSchema, CreatePipelineFromDescriptionSchema, CreatePipelineRunSchema, GetLatestPipelineRunSchema, GetPipelineRunSchema, ListPipelineRunsSchema, UpdatePipelineSchema,
|
|
45
|
+
// Pipeline job schemas
|
|
46
|
+
ListPipelineJobsByCategorySchema, ListPipelineJobHistorysSchema, ExecutePipelineJobRunSchema, GetPipelineJobRunLogSchema,
|
|
47
|
+
// Service connection schemas
|
|
48
|
+
ListServiceConnectionsSchema,
|
|
49
|
+
// Resource member schemas
|
|
50
|
+
DeleteResourceMemberSchema, UpdateResourceMemberSchema, CreateResourceMemberSchema, UpdateResourceOwnerSchema, ResourceMemberSchema, ResourceMemberBaseSchema,
|
|
51
|
+
// Tag schemas
|
|
52
|
+
CreateTagSchema, CreateTagGroupSchema, DeleteTagGroupSchema, UpdateTagGroupSchema, DeleteTagSchema, UpdateTagSchema, GetTagGroupSchema, TagGroupSchema, TagSchema, TagGroupWithTagsSchema, BaseTagSchema,
|
|
53
|
+
// VM Deploy Order schemas
|
|
54
|
+
StopVMDeployOrderSchema, SkipVMDeployMachineSchema, RetryVMDeployMachineSchema, ResumeVMDeployOrderSchema, GetVMDeployOrderSchema, GetVMDeployMachineLogSchema, DeployOrderSchema, DeployOrderLogSchema } from "../operations/flow/types.js";
|
|
55
|
+
// Packages types
|
|
56
|
+
export {
|
|
57
|
+
// Package repository schemas
|
|
58
|
+
ListPackageRepositoriesSchema,
|
|
59
|
+
// Artifact schemas
|
|
60
|
+
ListArtifactsSchema, GetArtifactSchema } from "../operations/packages/types.js";
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { getUserAgent } from "universal-user-agent";
|
|
2
|
+
import { createYunxiaoError } from "./errors.js";
|
|
3
|
+
import { VERSION } from "./version.js";
|
|
4
|
+
const DEFAULT_YUNXIAO_API_BASE_URL = "https://openapi-rdc.aliyuncs.com";
|
|
5
|
+
/**
|
|
6
|
+
* Get the Yunxiao API base URL from environment variables or use the default
|
|
7
|
+
* @returns The Yunxiao API base URL
|
|
8
|
+
*/
|
|
9
|
+
export function getYunxiaoApiBaseUrl() {
|
|
10
|
+
return process.env.YUNXIAO_API_BASE_URL || DEFAULT_YUNXIAO_API_BASE_URL;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 根据 YUNXIAO_API_BASE_URL 自动判断是否为 region 站:
|
|
14
|
+
* - 域名包含 openapi-rdc.aliyuncs.com -> 中心站
|
|
15
|
+
* - 其他域名 -> 认为是 region 站
|
|
16
|
+
*/
|
|
17
|
+
export function isRegionEdition() {
|
|
18
|
+
const baseUrl = getYunxiaoApiBaseUrl();
|
|
19
|
+
return !baseUrl.includes("openapi-rdc.aliyuncs.com");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* region 站下使用的"默认 organizationId"(占位值)
|
|
23
|
+
* - 默认使用 "default"
|
|
24
|
+
* - 如有需要,可以通过环境变量覆盖
|
|
25
|
+
*/
|
|
26
|
+
export function getRegionDefaultOrganizationId() {
|
|
27
|
+
return process.env.YUNXIAO_REGION_DEFAULT_ORG_ID || "default";
|
|
28
|
+
}
|
|
29
|
+
export function debug(message, data) {
|
|
30
|
+
if (data !== undefined) {
|
|
31
|
+
console.error(`[DEBUG] ${message}`, typeof data === 'object' ? JSON.stringify(data, null, 2) : data);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.error(`[DEBUG] ${message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function parseResponseBody(response) {
|
|
38
|
+
const contentType = response.headers.get("content-type");
|
|
39
|
+
if (contentType?.includes("application/json")) {
|
|
40
|
+
return response.json();
|
|
41
|
+
}
|
|
42
|
+
return response.text();
|
|
43
|
+
}
|
|
44
|
+
export function buildUrl(baseUrl, params) {
|
|
45
|
+
// Handle baseUrl that doesn't have protocol
|
|
46
|
+
const isAbsolute = baseUrl.startsWith("http://") || baseUrl.startsWith("https://");
|
|
47
|
+
const fullBaseUrl = isAbsolute ? baseUrl : `${getYunxiaoApiBaseUrl()}${baseUrl.startsWith('/') ? baseUrl : `/${baseUrl}`}`;
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(fullBaseUrl);
|
|
50
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
51
|
+
if (value !== undefined) {
|
|
52
|
+
url.searchParams.append(key, value.toString());
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const result = url.toString();
|
|
56
|
+
console.error(`[DEBUG] Final URL: ${result}`);
|
|
57
|
+
// If we started with a relative URL, return just the path portion
|
|
58
|
+
if (!baseUrl.startsWith('http')) {
|
|
59
|
+
// Extract the path and query string from the full URL
|
|
60
|
+
const urlObj = new URL(result);
|
|
61
|
+
return urlObj.pathname + urlObj.search;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`[ERROR] Failed to build URL: ${error}`);
|
|
67
|
+
// Fallback: manually append query parameters
|
|
68
|
+
let urlWithParams = baseUrl;
|
|
69
|
+
const queryParts = [];
|
|
70
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
71
|
+
if (value !== undefined) {
|
|
72
|
+
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (queryParts.length > 0) {
|
|
76
|
+
urlWithParams += (urlWithParams.includes('?') ? '&' : '?') + queryParts.join('&');
|
|
77
|
+
}
|
|
78
|
+
console.error(`[DEBUG] Fallback URL: ${urlWithParams}`);
|
|
79
|
+
return urlWithParams;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const USER_AGENT = `modelcontextprotocol/servers/alibabacloud-devops-mcp-server/v${VERSION} ${getUserAgent()}`;
|
|
83
|
+
let currentSessionToken = undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Set the token for the current session (used in SSE mode)
|
|
86
|
+
* @param yunxiao_access_token The token to use for the current session
|
|
87
|
+
*/
|
|
88
|
+
export function setCurrentSessionToken(yunxiao_access_token) {
|
|
89
|
+
currentSessionToken = yunxiao_access_token;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the token for the current session
|
|
93
|
+
* @returns The token for the current session, or the default token from environment
|
|
94
|
+
*/
|
|
95
|
+
export function getCurrentSessionToken() {
|
|
96
|
+
return currentSessionToken || process.env.YUNXIAO_ACCESS_TOKEN;
|
|
97
|
+
}
|
|
98
|
+
export async function yunxiaoRequest(urlPath, options = {}) {
|
|
99
|
+
// Check if the URL is already a full URL or a path
|
|
100
|
+
const isAbsolute = urlPath.startsWith("http://") || urlPath.startsWith("https://");
|
|
101
|
+
let url = isAbsolute ? urlPath : `${getYunxiaoApiBaseUrl()}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`;
|
|
102
|
+
const requestHeaders = {
|
|
103
|
+
"Accept": "application/json",
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
"User-Agent": USER_AGENT,
|
|
106
|
+
...options.headers,
|
|
107
|
+
};
|
|
108
|
+
// Use session-specific token if available, otherwise use default token
|
|
109
|
+
const token = getCurrentSessionToken();
|
|
110
|
+
if (token) {
|
|
111
|
+
requestHeaders["x-yunxiao-token"] = token;
|
|
112
|
+
}
|
|
113
|
+
debug(`Request: ${options.method} ${url}`);
|
|
114
|
+
debug(`Headers:`, requestHeaders);
|
|
115
|
+
debug(`Body:`, options.body);
|
|
116
|
+
const response = await fetch(url, {
|
|
117
|
+
method: options.method || "GET",
|
|
118
|
+
headers: requestHeaders,
|
|
119
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
120
|
+
});
|
|
121
|
+
const responseBody = await parseResponseBody(response);
|
|
122
|
+
debug(`Response Body:`, responseBody);
|
|
123
|
+
debug(`Response status:`, response.ok);
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw createYunxiaoError(response.status, responseBody, url, options.method || "GET", requestHeaders, options.body);
|
|
126
|
+
}
|
|
127
|
+
return responseBody;
|
|
128
|
+
}
|
|
129
|
+
export function pathEscape(filePath) {
|
|
130
|
+
// 先使用encodeURIComponent进行编码
|
|
131
|
+
let encoded = encodeURIComponent(filePath);
|
|
132
|
+
// 将编码后的%2F(/的编码)替换回/
|
|
133
|
+
encoded = encoded.replace(/%2F/gi, "/");
|
|
134
|
+
return encoded;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Handle repository ID encoding
|
|
138
|
+
* @param repositoryId Repository ID which may contain unencoded slash
|
|
139
|
+
* @returns Properly encoded repository ID
|
|
140
|
+
*/
|
|
141
|
+
export function handleRepositoryIdEncoding(repositoryId) {
|
|
142
|
+
let encodedRepoId = repositoryId;
|
|
143
|
+
// Automatically handle unencoded slashes in repositoryId
|
|
144
|
+
if (repositoryId.includes("/")) {
|
|
145
|
+
// Found unencoded slash, automatically URL encode it
|
|
146
|
+
const parts = repositoryId.split("/", 2);
|
|
147
|
+
if (parts.length === 2) {
|
|
148
|
+
const encodedRepoName = encodeURIComponent(parts[1]);
|
|
149
|
+
// Remove + signs from encoding (spaces are encoded as +, but we need %20)
|
|
150
|
+
const formattedEncodedName = encodedRepoName.replace(/\+/g, "%20");
|
|
151
|
+
encodedRepoId = `${parts[0]}%2F${formattedEncodedName}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return encodedRepoId;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Converts a floating point number to an integer string (removes decimal point and decimal part)
|
|
158
|
+
* Used primarily for handling numeric IDs that might come as floats from JSON parsing
|
|
159
|
+
* @param value Value to convert
|
|
160
|
+
* @returns Integer string representation
|
|
161
|
+
*/
|
|
162
|
+
export function floatToIntString(value) {
|
|
163
|
+
// 如果传入的是字符串,先尝试转为浮点数
|
|
164
|
+
if (typeof value === 'string') {
|
|
165
|
+
const floatValue = parseFloat(value);
|
|
166
|
+
if (!isNaN(floatValue)) {
|
|
167
|
+
value = floatValue;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
return value; // 如果转换失败,返回原字符串
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// 处理浮点数
|
|
174
|
+
if (typeof value === 'number') {
|
|
175
|
+
const intValue = Math.floor(value + 0.5); // 四舍五入转整数
|
|
176
|
+
return intValue.toString();
|
|
177
|
+
}
|
|
178
|
+
// 处理其他情况,直接转字符串
|
|
179
|
+
return String(value);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 将各种时间格式转换为毫秒时间戳
|
|
183
|
+
* 支持:
|
|
184
|
+
* - 已有时间戳(number)直接返回
|
|
185
|
+
* - Date对象转换为时间戳
|
|
186
|
+
* - ISO格式日期字符串 (如: '2023-01-01T00:00:00Z')
|
|
187
|
+
* - 日期字符串 (如: '2023-01-01')
|
|
188
|
+
*
|
|
189
|
+
* @param time 时间输入
|
|
190
|
+
* @returns 毫秒时间戳
|
|
191
|
+
*/
|
|
192
|
+
export function convertToTimestamp(time) {
|
|
193
|
+
if (typeof time === 'number') {
|
|
194
|
+
// 如果已经是数字,假设已是时间戳
|
|
195
|
+
return time;
|
|
196
|
+
}
|
|
197
|
+
else if (time instanceof Date) {
|
|
198
|
+
// 如果是Date对象,转换为时间戳
|
|
199
|
+
return time.getTime();
|
|
200
|
+
}
|
|
201
|
+
else if (typeof time === 'string') {
|
|
202
|
+
// 尝试解析日期字符串
|
|
203
|
+
const date = new Date(time);
|
|
204
|
+
if (!isNaN(date.getTime())) {
|
|
205
|
+
return date.getTime();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// 无法转换时返回原值(如果是数字)或当前时间戳
|
|
209
|
+
return typeof time === 'number' ? time : Date.now();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get start of today timestamp
|
|
213
|
+
* @returns Timestamp for start of the current day (00:00:00)
|
|
214
|
+
*/
|
|
215
|
+
export function getStartOfTodayTimestamp() {
|
|
216
|
+
const now = new Date();
|
|
217
|
+
// Reset time to start of day (00:00:00.000)
|
|
218
|
+
now.setHours(0, 0, 0, 0);
|
|
219
|
+
return now.getTime();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get end of today timestamp
|
|
223
|
+
* @returns Timestamp for end of the current day (23:59:59.999)
|
|
224
|
+
*/
|
|
225
|
+
export function getEndOfTodayTimestamp() {
|
|
226
|
+
const now = new Date();
|
|
227
|
+
// Set time to end of day (23:59:59.999)
|
|
228
|
+
now.setHours(23, 59, 59, 999);
|
|
229
|
+
return now.getTime();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get timestamp for start of a specific day
|
|
233
|
+
* @param date Date object or date string
|
|
234
|
+
* @returns Timestamp for start of the specified day
|
|
235
|
+
*/
|
|
236
|
+
export function getStartOfDayTimestamp(date) {
|
|
237
|
+
const targetDate = typeof date === 'string' ? new Date(date) : new Date(date);
|
|
238
|
+
targetDate.setHours(0, 0, 0, 0);
|
|
239
|
+
return targetDate.getTime();
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get timestamp for end of a specific day
|
|
243
|
+
* @param date Date object or date string
|
|
244
|
+
* @returns Timestamp for end of the specified day
|
|
245
|
+
*/
|
|
246
|
+
export function getEndOfDayTimestamp(date) {
|
|
247
|
+
const targetDate = typeof date === 'string' ? new Date(date) : new Date(date);
|
|
248
|
+
targetDate.setHours(23, 59, 59, 999);
|
|
249
|
+
return targetDate.getTime();
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get timestamp for start of current week
|
|
253
|
+
* @param startOnMonday Whether week should start on Monday (true) or Sunday (false)
|
|
254
|
+
* @returns Timestamp for start of the current week
|
|
255
|
+
*/
|
|
256
|
+
export function getStartOfWeekTimestamp(startOnMonday = true) {
|
|
257
|
+
const now = new Date();
|
|
258
|
+
const dayOfWeek = now.getDay(); // 0 is Sunday, 1 is Monday, etc.
|
|
259
|
+
const diff = startOnMonday ?
|
|
260
|
+
(dayOfWeek === 0 ? 6 : dayOfWeek - 1) : // If startOnMonday, set Sunday as day 7
|
|
261
|
+
dayOfWeek;
|
|
262
|
+
// Set to beginning of the week
|
|
263
|
+
now.setDate(now.getDate() - diff);
|
|
264
|
+
now.setHours(0, 0, 0, 0);
|
|
265
|
+
return now.getTime();
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get timestamp for end of current week
|
|
269
|
+
* @param startOnMonday Whether week should start on Monday (true) or Sunday (false)
|
|
270
|
+
* @returns Timestamp for end of the current week
|
|
271
|
+
*/
|
|
272
|
+
export function getEndOfWeekTimestamp(startOnMonday = true) {
|
|
273
|
+
const now = new Date();
|
|
274
|
+
const dayOfWeek = now.getDay(); // 0 is Sunday, 1 is Monday, etc.
|
|
275
|
+
const diff = startOnMonday ?
|
|
276
|
+
(dayOfWeek === 0 ? 0 : 7 - dayOfWeek) : // If startOnMonday, set Sunday as day 7
|
|
277
|
+
(6 - dayOfWeek);
|
|
278
|
+
// Set to end of the week
|
|
279
|
+
now.setDate(now.getDate() + diff);
|
|
280
|
+
now.setHours(23, 59, 59, 999);
|
|
281
|
+
return now.getTime();
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get timestamp for start of current month
|
|
285
|
+
* @returns Timestamp for start of the current month
|
|
286
|
+
*/
|
|
287
|
+
export function getStartOfMonthTimestamp() {
|
|
288
|
+
const now = new Date();
|
|
289
|
+
now.setDate(1); // First day of current month
|
|
290
|
+
now.setHours(0, 0, 0, 0);
|
|
291
|
+
return now.getTime();
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get timestamp for end of current month
|
|
295
|
+
* @returns Timestamp for end of the current month
|
|
296
|
+
*/
|
|
297
|
+
export function getEndOfMonthTimestamp() {
|
|
298
|
+
const now = new Date();
|
|
299
|
+
now.setMonth(now.getMonth() + 1); // Move to next month
|
|
300
|
+
now.setDate(0); // Last day of previous month (i.e., current month)
|
|
301
|
+
now.setHours(23, 59, 59, 999);
|
|
302
|
+
return now.getTime();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Analyzes natural language date reference and returns corresponding timestamp range
|
|
306
|
+
* @param dateReference Natural language date reference (e.g., "today", "this week", "last month")
|
|
307
|
+
* @returns Object containing start and end timestamps
|
|
308
|
+
*/
|
|
309
|
+
export function parseDateReference(dateReference) {
|
|
310
|
+
if (!dateReference) {
|
|
311
|
+
// Default to all time
|
|
312
|
+
return {
|
|
313
|
+
startTime: 0,
|
|
314
|
+
endTime: Date.now()
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const normalizedRef = dateReference.trim().toLowerCase();
|
|
318
|
+
// Today/yesterday
|
|
319
|
+
if (normalizedRef === 'today' || normalizedRef === '今天') {
|
|
320
|
+
return {
|
|
321
|
+
startTime: getStartOfTodayTimestamp(),
|
|
322
|
+
endTime: getEndOfTodayTimestamp()
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (normalizedRef === 'yesterday' || normalizedRef === '昨天') {
|
|
326
|
+
const yesterday = new Date();
|
|
327
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
328
|
+
return {
|
|
329
|
+
startTime: getStartOfDayTimestamp(yesterday),
|
|
330
|
+
endTime: getEndOfDayTimestamp(yesterday)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// This week/last week
|
|
334
|
+
if (normalizedRef === 'this week' || normalizedRef === '本周' ||
|
|
335
|
+
normalizedRef === 'current week' || normalizedRef === '这周' ||
|
|
336
|
+
normalizedRef === '这个星期') {
|
|
337
|
+
return {
|
|
338
|
+
startTime: getStartOfWeekTimestamp(),
|
|
339
|
+
endTime: getEndOfWeekTimestamp()
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (normalizedRef === 'last week' || normalizedRef === '上周' ||
|
|
343
|
+
normalizedRef === '上個星期' || normalizedRef === '上个星期') {
|
|
344
|
+
const lastWeekStart = new Date(getStartOfWeekTimestamp());
|
|
345
|
+
lastWeekStart.setDate(lastWeekStart.getDate() - 7);
|
|
346
|
+
const lastWeekEnd = new Date(getEndOfWeekTimestamp());
|
|
347
|
+
lastWeekEnd.setDate(lastWeekEnd.getDate() - 7);
|
|
348
|
+
return {
|
|
349
|
+
startTime: lastWeekStart.getTime(),
|
|
350
|
+
endTime: lastWeekEnd.getTime()
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// This month/last month
|
|
354
|
+
if (normalizedRef === 'this month' || normalizedRef === '本月' ||
|
|
355
|
+
normalizedRef === 'current month' || normalizedRef === '这个月') {
|
|
356
|
+
return {
|
|
357
|
+
startTime: getStartOfMonthTimestamp(),
|
|
358
|
+
endTime: getEndOfMonthTimestamp()
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (normalizedRef === 'last month' || normalizedRef === '上月' ||
|
|
362
|
+
normalizedRef === '上个月') {
|
|
363
|
+
const now = new Date();
|
|
364
|
+
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1);
|
|
365
|
+
// Start of last month
|
|
366
|
+
const startOfLastMonth = new Date(lastMonth.getFullYear(), lastMonth.getMonth(), 1);
|
|
367
|
+
startOfLastMonth.setHours(0, 0, 0, 0);
|
|
368
|
+
// End of last month
|
|
369
|
+
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0);
|
|
370
|
+
endOfLastMonth.setHours(23, 59, 59, 999);
|
|
371
|
+
return {
|
|
372
|
+
startTime: startOfLastMonth.getTime(),
|
|
373
|
+
endTime: endOfLastMonth.getTime()
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
// Default to all time
|
|
377
|
+
return {
|
|
378
|
+
startTime: 0,
|
|
379
|
+
endTime: Date.now()
|
|
380
|
+
};
|
|
381
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = "0.1.17";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { isYunxiaoError, YunxiaoValidationError } from "./common/errors.js";
|
|
8
|
+
import { VERSION } from "./common/version.js";
|
|
9
|
+
import { config } from "dotenv";
|
|
10
|
+
import { getAllTools, getEnabledTools } from "./tool-registry/index.js";
|
|
11
|
+
import { handleToolRequest, handleEnabledToolRequest } from "./tool-handlers/index.js";
|
|
12
|
+
import { Toolset } from "./common/toolsets.js";
|
|
13
|
+
const server = new Server({
|
|
14
|
+
name: "alibabacloud-devops-mcp-server",
|
|
15
|
+
version: VERSION,
|
|
16
|
+
}, {
|
|
17
|
+
capabilities: {
|
|
18
|
+
tools: {},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
function formatYunxiaoError(error) {
|
|
22
|
+
let message = `Yunxiao API Error: ${error.message}`;
|
|
23
|
+
// 添加请求上下文信息
|
|
24
|
+
if (error.method || error.url) {
|
|
25
|
+
message += `\n Request: ${error.method || 'GET'} ${error.url || 'unknown'}`;
|
|
26
|
+
}
|
|
27
|
+
if (error.requestHeaders) {
|
|
28
|
+
message += `\n Request Headers: ${JSON.stringify(error.requestHeaders, null, 2)}`;
|
|
29
|
+
}
|
|
30
|
+
if (error.requestBody) {
|
|
31
|
+
message += `\n Request Body: ${JSON.stringify(error.requestBody, null, 2)}`;
|
|
32
|
+
}
|
|
33
|
+
if (error instanceof YunxiaoValidationError) {
|
|
34
|
+
message = `Parameter validation failed: ${error.message}`;
|
|
35
|
+
if (error.response) {
|
|
36
|
+
message += `\n Response: ${JSON.stringify(error.response, null, 2)}`;
|
|
37
|
+
}
|
|
38
|
+
// 添加常见参数错误的提示
|
|
39
|
+
if (error.message.includes('name')) {
|
|
40
|
+
message += `\n Suggestion: Please check whether the pipeline name meets the requirements.`;
|
|
41
|
+
}
|
|
42
|
+
if (error.message.includes('content') || error.message.includes('yaml')) {
|
|
43
|
+
message += `\n Suggestion: Please check whether the generated YAML format is correct.`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// 处理通用的Yunxiao错误
|
|
48
|
+
message = `Yunxiao API error (${error.status}): ${error.message}`;
|
|
49
|
+
if (error.response) {
|
|
50
|
+
const response = error.response;
|
|
51
|
+
if (response.errorCode) {
|
|
52
|
+
message += `\n errorCode: ${response.errorCode}`;
|
|
53
|
+
}
|
|
54
|
+
if (response.errorMessage && response.errorMessage !== error.message) {
|
|
55
|
+
message += `\n errorMessage: ${response.errorMessage}`;
|
|
56
|
+
}
|
|
57
|
+
if (response.data && typeof response.data === 'object') {
|
|
58
|
+
message += `\n data: ${JSON.stringify(response.data, null, 2)}`;
|
|
59
|
+
}
|
|
60
|
+
// 如果响应体中有更多详细信息,也一并显示
|
|
61
|
+
if (Object.keys(response).length > 0) {
|
|
62
|
+
message += `\n Full Response: ${JSON.stringify(response, null, 2)}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 根据状态码提供通用建议
|
|
66
|
+
switch (error.status) {
|
|
67
|
+
case 400:
|
|
68
|
+
message += `\n Suggestion: Please check whether the request parameters are correct, especially whether all required parameters have been provided.`;
|
|
69
|
+
break;
|
|
70
|
+
case 500:
|
|
71
|
+
message += `\n Suggestion: Internal server error. Please try again later or contact technical support.`;
|
|
72
|
+
break;
|
|
73
|
+
case 502:
|
|
74
|
+
case 503:
|
|
75
|
+
case 504:
|
|
76
|
+
message += `\n Suggestion: The service is temporarily unavailable. Please try again later.`;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return message;
|
|
81
|
+
}
|
|
82
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
83
|
+
let tools;
|
|
84
|
+
if (enabledToolsets.length > 0) {
|
|
85
|
+
// 获取基础工具(总是加载)
|
|
86
|
+
const baseTools = getEnabledTools([Toolset.BASE]);
|
|
87
|
+
// 获取启用的工具集工具
|
|
88
|
+
const enabledTools = getEnabledTools(enabledToolsets);
|
|
89
|
+
// 合并基础工具和启用的工具集工具
|
|
90
|
+
tools = [...baseTools, ...enabledTools];
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// 如果没有指定启用的工具集,则获取所有工具(已包含基础工具)
|
|
94
|
+
tools = getAllTools();
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
tools,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
101
|
+
try {
|
|
102
|
+
if (!request.params.arguments) {
|
|
103
|
+
throw new Error("Arguments are required");
|
|
104
|
+
}
|
|
105
|
+
// Delegate to our modular tool handler with toolset support
|
|
106
|
+
const result = enabledToolsets.length > 0
|
|
107
|
+
? await handleEnabledToolRequest(request, enabledToolsets)
|
|
108
|
+
: await handleToolRequest(request);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error instanceof z.ZodError) {
|
|
113
|
+
throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`);
|
|
114
|
+
}
|
|
115
|
+
if (isYunxiaoError(error)) {
|
|
116
|
+
throw new Error(formatYunxiaoError(error));
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
config();
|
|
122
|
+
// 解析启用的工具集
|
|
123
|
+
const parseEnabledToolsets = (input) => {
|
|
124
|
+
if (!input)
|
|
125
|
+
return [];
|
|
126
|
+
return input.split(',').map(toolset => {
|
|
127
|
+
const trimmed = toolset.trim();
|
|
128
|
+
// 验证工具集名称是否有效
|
|
129
|
+
if (!Object.values(Toolset).includes(trimmed)) {
|
|
130
|
+
throw new Error(`Unknown toolset: ${trimmed}`);
|
|
131
|
+
}
|
|
132
|
+
return trimmed;
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
// 获取启用的工具集(从命令行参数或环境变量)
|
|
136
|
+
const enabledToolsets = parseEnabledToolsets(process.argv.find(arg => arg.startsWith('--toolsets='))?.split('=')[1] ||
|
|
137
|
+
process.env.DEVOPS_TOOLSETS);
|
|
138
|
+
// Check if we should run in SSE mode
|
|
139
|
+
const useSSE = process.argv.includes('--sse') || process.env.MCP_TRANSPORT === 'sse';
|
|
140
|
+
async function runServer() {
|
|
141
|
+
if (useSSE) {
|
|
142
|
+
// Import express only when needed for SSE mode
|
|
143
|
+
const { default: express } = await import('express');
|
|
144
|
+
const app = express();
|
|
145
|
+
const port = process.env.PORT || 3000;
|
|
146
|
+
// Store sessions with their tokens
|
|
147
|
+
const sessions = {};
|
|
148
|
+
// SSE endpoint - handles initial connection
|
|
149
|
+
app.get('/sse', async (req, res) => {
|
|
150
|
+
// In SSE mode, we can use console.log for debugging since it doesn't interfere with the protocol
|
|
151
|
+
console.log(`New SSE connection from ${req.ip}`);
|
|
152
|
+
// Get token from query parameters or headers
|
|
153
|
+
const yunxiao_access_token = req.query.yunxiao_access_token || req.headers['x-yunxiao-token'] || process.env.YUNXIAO_ACCESS_TOKEN;
|
|
154
|
+
// Create transport with endpoint for POST messages
|
|
155
|
+
const sseTransport = new SSEServerTransport('/messages', res);
|
|
156
|
+
const sessionId = sseTransport.sessionId;
|
|
157
|
+
if (sessionId) {
|
|
158
|
+
sessions[sessionId] = { transport: sseTransport, server, yunxiao_access_token };
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await server.connect(sseTransport);
|
|
162
|
+
// In SSE mode, console.error is acceptable for status messages
|
|
163
|
+
console.info(`Yunxiao MCP Server connected via SSE with session ${sessionId}`);
|
|
164
|
+
if (yunxiao_access_token) {
|
|
165
|
+
console.error(`Session ${sessionId} using custom token`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
console.error(`Session ${sessionId} using default token from environment`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
console.error("Failed to start SSE server:", error);
|
|
173
|
+
res.status(500).send("Server error");
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// POST endpoint - handles incoming messages
|
|
177
|
+
app.use(express.json({ limit: '10mb' })); // Add JSON body parser
|
|
178
|
+
app.post('/messages', async (req, res) => {
|
|
179
|
+
const sessionId = req.query.sessionId;
|
|
180
|
+
const session = sessions[sessionId];
|
|
181
|
+
if (!session) {
|
|
182
|
+
res.status(404).send("Session not found");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
// Set the session token before handling the message
|
|
187
|
+
const utils = await import('./common/utils.js');
|
|
188
|
+
utils.setCurrentSessionToken(session.yunxiao_access_token);
|
|
189
|
+
await session.transport.handlePostMessage(req, res, req.body);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.error("Error handling POST message:", error);
|
|
193
|
+
res.status(500).send("Server error");
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
// Start server
|
|
197
|
+
const serverInstance = app.listen(port, () => {
|
|
198
|
+
console.log(`Yunxiao MCP Server running in SSE mode on port ${port}`);
|
|
199
|
+
console.log(`Connect via SSE at http://localhost:${port}/sse`);
|
|
200
|
+
console.log(`Send messages to http://localhost:${port}/messages?sessionId=<session-id>`);
|
|
201
|
+
});
|
|
202
|
+
// Handle graceful shutdown
|
|
203
|
+
process.on('SIGINT', () => {
|
|
204
|
+
console.log('Shutting down SSE server...');
|
|
205
|
+
serverInstance.close(() => {
|
|
206
|
+
console.log('Server closed.');
|
|
207
|
+
process.exit(0);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Stdio mode (default)
|
|
213
|
+
// In stdio mode, we must avoid console.log/console.error as they interfere with the JSON-RPC protocol
|
|
214
|
+
const transport = new StdioServerTransport();
|
|
215
|
+
await server.connect(transport);
|
|
216
|
+
// Don't output anything to stdout/stderr in stdio mode - only JSON-RPC messages should go through the transport
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
runServer().catch((error) => {
|
|
220
|
+
// Only output error to stderr in SSE mode, not in stdio mode
|
|
221
|
+
if (useSSE) {
|
|
222
|
+
console.error("Fatal error in main():", error);
|
|
223
|
+
}
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|