@daghis/teamcity-mcp 0.9.2 → 1.0.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.1](https://github.com/Daghis/teamcity-mcp/compare/v1.0.0...v1.0.1) (2025-09-12)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **tools:** update_build_config uses 'settings/artifactRules' path; add tests ([#66](https://github.com/Daghis/teamcity-mcp/issues/66)) ([8b8afc6](https://github.com/Daghis/teamcity-mcp/commit/8b8afc6f41038bcde21a50a8662f90fa4acb7e9a))
9
+
10
+ ## [1.0.0](https://github.com/Daghis/teamcity-mcp/compare/v0.9.2...v1.0.0) (2025-09-12)
11
+
12
+ ### Features
13
+ - 1.0 release: stabilize API surface and defaults
14
+
15
+ ### Docs
16
+ - Add Claude Code setup command and context usage estimates (dev vs full)
17
+
3
18
  ## [0.9.2](https://github.com/Daghis/teamcity-mcp/compare/v0.9.1...v0.9.2) (2025-09-12)
4
19
 
5
20
  ### Bug Fixes
package/README.md CHANGED
@@ -81,13 +81,15 @@ npx -y @daghis/teamcity-mcp
81
81
  npx -y @daghis/teamcity-mcp
82
82
  ```
83
83
 
84
- Register with Claude Code’s MCP (user scope):
85
-
86
- ```bash
87
- claude mcp add teamcity -s user \
88
- -- env TEAMCITY_URL="https://teamcity.example.com" TEAMCITY_TOKEN="tc_<your_token>" MCP_MODE=dev \
89
- npx -y @daghis/teamcity-mcp
90
- ```
84
+ ## Claude Code
85
+
86
+ - Add the MCP:
87
+ - `claude mcp add [-s user] teamcity -- npx -y @daghis/teamcity-mcp`
88
+ - With env vars (if not using .env):
89
+ - `claude mcp add [-s user] teamcity -- env TEAMCITY_URL="https://teamcity.example.com" TEAMCITY_TOKEN="tc_<your_token>" MCP_MODE=dev npx -y @daghis/teamcity-mcp`
90
+ - Context usage (Opus 4.1, estimates):
91
+ - Dev (default): ~14k tokens for MCP tools
92
+ - Full (`MCP_MODE=full`): ~26k tokens for MCP tools
91
93
 
92
94
  ## Configuration
93
95
 
package/dist/index.js CHANGED
@@ -579,7 +579,7 @@ function loadConfig() {
579
579
  },
580
580
  mcp: {
581
581
  name: "teamcity-mcp",
582
- version: "0.1.0",
582
+ version: "1.0.0",
583
583
  protocolVersion: "1.0.0",
584
584
  capabilities: {
585
585
  tools: true,
@@ -924,6 +924,388 @@ function debug(message, meta) {
924
924
  // src/tools.ts
925
925
  var import_zod4 = require("zod");
926
926
 
927
+ // src/teamcity/build-configuration-update-manager.ts
928
+ var BuildConfigurationUpdateManager = class {
929
+ client;
930
+ constructor(client) {
931
+ this.client = client;
932
+ }
933
+ /**
934
+ * Retrieve current build configuration
935
+ */
936
+ async retrieveConfiguration(configId) {
937
+ try {
938
+ const response = await this.client.buildTypes.getBuildType(
939
+ configId,
940
+ "$long,parameters($long),settings($long),agent-requirements($long)"
941
+ );
942
+ if (response.data == null) {
943
+ return null;
944
+ }
945
+ const config2 = response.data;
946
+ const parameters = {};
947
+ if (config2.parameters?.property != null) {
948
+ for (const param of config2.parameters.property) {
949
+ if (param.name != null && param.value != null) {
950
+ parameters[param.name] = param.value;
951
+ }
952
+ }
953
+ }
954
+ const buildNumberFormat = config2.settings?.property?.find(
955
+ (p) => p.name === "buildNumberPattern"
956
+ )?.value;
957
+ const artifactRules = config2.settings?.property?.find(
958
+ (p) => p.name === "artifactRules"
959
+ )?.value;
960
+ const cleanBuild = config2.settings?.property?.find((p) => p.name === "cleanBuild")?.value === "true";
961
+ const executionTimeout = config2.settings?.property?.find(
962
+ (p) => p.name === "executionTimeoutMin"
963
+ )?.value;
964
+ const checkoutDirectory = config2.settings?.property?.find(
965
+ (p) => p.name === "checkoutDirectory"
966
+ )?.value;
967
+ if (!config2.id || !config2.name) {
968
+ throw new Error("Invalid configuration data: missing id or name");
969
+ }
970
+ return {
971
+ id: config2.id,
972
+ name: config2.name,
973
+ description: config2.description,
974
+ projectId: config2.projectId ?? config2.project?.id ?? "",
975
+ buildNumberFormat,
976
+ artifactRules,
977
+ parameters,
978
+ agentRequirements: config2["agent-requirements"],
979
+ buildOptions: {
980
+ cleanBuild,
981
+ executionTimeout: executionTimeout != null ? parseInt(executionTimeout, 10) : void 0,
982
+ checkoutDirectory
983
+ },
984
+ settings: config2.settings
985
+ };
986
+ } catch (err) {
987
+ if (err != null && typeof err === "object" && "response" in err && err.response?.status === 404) {
988
+ debug("Build configuration not found", { configId });
989
+ return null;
990
+ }
991
+ if (err != null && typeof err === "object" && "response" in err && err.response?.status === 403) {
992
+ throw new Error("Permission denied: No access to build configuration");
993
+ }
994
+ throw err;
995
+ }
996
+ }
997
+ /**
998
+ * Validate updates before applying
999
+ */
1000
+ async validateUpdates(currentConfig, updates) {
1001
+ debug("Validating updates", {
1002
+ configId: currentConfig.id,
1003
+ updateFields: Object.keys(updates)
1004
+ });
1005
+ if (updates.parameters) {
1006
+ for (const paramName of Object.keys(updates.parameters)) {
1007
+ if (!this.isValidParameterName(paramName)) {
1008
+ throw new Error(`Invalid parameter name: ${paramName}`);
1009
+ }
1010
+ }
1011
+ }
1012
+ if (updates.removeParameters) {
1013
+ for (const paramName of updates.removeParameters) {
1014
+ if (!currentConfig.parameters?.[paramName]) {
1015
+ throw new Error(`Parameter does not exist: ${paramName}`);
1016
+ }
1017
+ }
1018
+ }
1019
+ if (updates.parameters && updates.removeParameters) {
1020
+ const addOrUpdate = Object.keys(updates.parameters);
1021
+ const toRemove = updates.removeParameters;
1022
+ const conflicts = addOrUpdate.filter((param) => toRemove.includes(param));
1023
+ if (conflicts.length > 0) {
1024
+ throw new Error(
1025
+ `Conflict: Cannot update and remove the same parameter: ${conflicts.join(", ")}`
1026
+ );
1027
+ }
1028
+ }
1029
+ if (updates.buildNumberFormat) {
1030
+ if (!this.isValidBuildNumberFormat(updates.buildNumberFormat)) {
1031
+ throw new Error(`Invalid build number format: ${updates.buildNumberFormat}`);
1032
+ }
1033
+ }
1034
+ if (updates.artifactRules) {
1035
+ if (!this.isValidArtifactRules(updates.artifactRules)) {
1036
+ throw new Error(`Invalid artifact rules: ${updates.artifactRules}`);
1037
+ }
1038
+ }
1039
+ if (updates.buildOptions?.executionTimeout !== void 0) {
1040
+ if (updates.buildOptions.executionTimeout < 0 || updates.buildOptions.executionTimeout > 1440) {
1041
+ throw new Error("Execution timeout must be between 0 and 1440 minutes");
1042
+ }
1043
+ }
1044
+ return true;
1045
+ }
1046
+ /**
1047
+ * Apply updates to configuration
1048
+ */
1049
+ async applyUpdates(currentConfig, updates) {
1050
+ info("Applying updates to build configuration", {
1051
+ id: currentConfig.id,
1052
+ updateCount: Object.keys(updates).length
1053
+ });
1054
+ const configPayload = {
1055
+ id: currentConfig.id,
1056
+ name: updates.name ?? currentConfig.name,
1057
+ description: updates.description ?? currentConfig.description,
1058
+ project: {
1059
+ id: currentConfig.projectId
1060
+ }
1061
+ };
1062
+ const settings = [];
1063
+ if (updates.buildNumberFormat !== void 0) {
1064
+ settings.push({
1065
+ name: "buildNumberPattern",
1066
+ value: updates.buildNumberFormat
1067
+ });
1068
+ }
1069
+ if (updates.artifactRules !== void 0) {
1070
+ settings.push({
1071
+ name: "artifactRules",
1072
+ value: updates.artifactRules
1073
+ });
1074
+ }
1075
+ if (updates.buildOptions) {
1076
+ if (updates.buildOptions.cleanBuild !== void 0) {
1077
+ settings.push({
1078
+ name: "cleanBuild",
1079
+ value: updates.buildOptions.cleanBuild.toString()
1080
+ });
1081
+ }
1082
+ if (updates.buildOptions.executionTimeout !== void 0) {
1083
+ settings.push({
1084
+ name: "executionTimeoutMin",
1085
+ value: updates.buildOptions.executionTimeout.toString()
1086
+ });
1087
+ }
1088
+ if (updates.buildOptions.checkoutDirectory !== void 0) {
1089
+ settings.push({
1090
+ name: "checkoutDirectory",
1091
+ value: updates.buildOptions.checkoutDirectory
1092
+ });
1093
+ }
1094
+ }
1095
+ if (settings.length > 0) {
1096
+ configPayload.settings = { property: settings };
1097
+ }
1098
+ const finalParameters = { ...currentConfig.parameters };
1099
+ if (updates.removeParameters) {
1100
+ for (const paramName of updates.removeParameters) {
1101
+ delete finalParameters[paramName];
1102
+ }
1103
+ }
1104
+ if (updates.parameters) {
1105
+ Object.assign(finalParameters, updates.parameters);
1106
+ }
1107
+ if ((updates.parameters ?? updates.removeParameters) != null) {
1108
+ configPayload.parameters = {
1109
+ property: Object.entries(finalParameters).map(([name, value]) => ({
1110
+ name,
1111
+ value
1112
+ }))
1113
+ };
1114
+ }
1115
+ if (updates.agentRequirements) {
1116
+ debug("Agent requirements update requested", updates.agentRequirements);
1117
+ }
1118
+ try {
1119
+ if (updates.name !== void 0 || updates.description !== void 0) {
1120
+ if (updates.name) {
1121
+ await this.client.buildTypes.setBuildTypeField(currentConfig.id, "name", updates.name);
1122
+ }
1123
+ if (updates.description !== void 0) {
1124
+ await this.client.buildTypes.setBuildTypeField(
1125
+ currentConfig.id,
1126
+ "description",
1127
+ updates.description ?? ""
1128
+ );
1129
+ }
1130
+ }
1131
+ if (settings.length > 0) {
1132
+ for (const setting of settings) {
1133
+ await this.client.buildTypes.setBuildTypeField(
1134
+ currentConfig.id,
1135
+ `settings/${setting.name}`,
1136
+ setting.value
1137
+ );
1138
+ }
1139
+ }
1140
+ if (updates.removeParameters) {
1141
+ for (const paramName of updates.removeParameters) {
1142
+ try {
1143
+ await this.client.buildTypes.deleteBuildParameterOfBuildType(
1144
+ paramName,
1145
+ currentConfig.id
1146
+ );
1147
+ } catch (err) {
1148
+ debug(`Failed to remove parameter ${paramName}`, err);
1149
+ }
1150
+ }
1151
+ }
1152
+ if (updates.parameters) {
1153
+ for (const [name, value] of Object.entries(updates.parameters)) {
1154
+ await this.client.buildTypes.setBuildTypeField(
1155
+ currentConfig.id,
1156
+ `parameters/${name}`,
1157
+ value
1158
+ );
1159
+ }
1160
+ }
1161
+ const updatedConfig = await this.retrieveConfiguration(currentConfig.id);
1162
+ if (!updatedConfig) {
1163
+ throw new Error("Failed to retrieve updated configuration");
1164
+ }
1165
+ info("Configuration updated successfully", {
1166
+ id: updatedConfig.id,
1167
+ name: updatedConfig.name
1168
+ });
1169
+ return updatedConfig;
1170
+ } catch (err) {
1171
+ const error2 = err;
1172
+ if (error2.response?.status === 409) {
1173
+ throw new Error("Configuration was modified by another user");
1174
+ }
1175
+ if (error2.response?.status === 403) {
1176
+ throw new Error("Permission denied: You need project edit permissions");
1177
+ }
1178
+ if (error2.response?.status === 400) {
1179
+ const message = error2.response?.data?.message ?? "Invalid configuration";
1180
+ throw new Error(`Invalid update: ${message}`);
1181
+ }
1182
+ error("Failed to apply updates", error2);
1183
+ throw new Error("Partial update failure");
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Generate change log comparing before and after states
1188
+ */
1189
+ generateChangeLog(currentConfig, updates) {
1190
+ const changeLog = {};
1191
+ if (updates.name && updates.name !== currentConfig.name) {
1192
+ changeLog["name"] = {
1193
+ before: currentConfig.name,
1194
+ after: updates.name
1195
+ };
1196
+ }
1197
+ if (updates.description !== void 0 && updates.description !== currentConfig.description) {
1198
+ changeLog["description"] = {
1199
+ before: currentConfig.description ?? "",
1200
+ after: updates.description
1201
+ };
1202
+ }
1203
+ if (updates.buildNumberFormat !== void 0 && updates.buildNumberFormat !== currentConfig.buildNumberFormat) {
1204
+ changeLog["buildNumberFormat"] = {
1205
+ before: currentConfig.buildNumberFormat ?? "",
1206
+ after: updates.buildNumberFormat
1207
+ };
1208
+ }
1209
+ if (updates.artifactRules !== void 0 && updates.artifactRules !== currentConfig.artifactRules) {
1210
+ changeLog["artifactRules"] = {
1211
+ before: currentConfig.artifactRules ?? "",
1212
+ after: updates.artifactRules
1213
+ };
1214
+ }
1215
+ if ((updates.parameters ?? updates.removeParameters) != null) {
1216
+ const paramChanges = {};
1217
+ if (updates.parameters) {
1218
+ const added = {};
1219
+ const updated = {};
1220
+ for (const [key, value] of Object.entries(updates.parameters)) {
1221
+ if (!currentConfig.parameters?.[key]) {
1222
+ added[key] = value;
1223
+ } else if (currentConfig.parameters[key] !== value) {
1224
+ updated[key] = {
1225
+ before: currentConfig.parameters[key],
1226
+ after: value
1227
+ };
1228
+ }
1229
+ }
1230
+ if (Object.keys(added).length > 0) {
1231
+ paramChanges.added = added;
1232
+ }
1233
+ if (Object.keys(updated).length > 0) {
1234
+ paramChanges.updated = updated;
1235
+ }
1236
+ }
1237
+ if (updates.removeParameters && updates.removeParameters.length > 0) {
1238
+ paramChanges.removed = updates.removeParameters;
1239
+ }
1240
+ if (Object.keys(paramChanges).length > 0) {
1241
+ changeLog["parameters"] = paramChanges;
1242
+ }
1243
+ }
1244
+ if (updates.buildOptions) {
1245
+ const optionChanges = {};
1246
+ if (updates.buildOptions.cleanBuild !== void 0 && updates.buildOptions.cleanBuild !== currentConfig.buildOptions?.cleanBuild) {
1247
+ optionChanges["cleanBuild"] = {
1248
+ before: currentConfig.buildOptions?.cleanBuild ?? false,
1249
+ after: updates.buildOptions.cleanBuild
1250
+ };
1251
+ }
1252
+ if (updates.buildOptions.executionTimeout !== void 0 && updates.buildOptions.executionTimeout !== currentConfig.buildOptions?.executionTimeout) {
1253
+ optionChanges["executionTimeout"] = {
1254
+ before: currentConfig.buildOptions?.executionTimeout ?? 0,
1255
+ after: updates.buildOptions.executionTimeout
1256
+ };
1257
+ }
1258
+ if (updates.buildOptions.checkoutDirectory !== void 0 && updates.buildOptions.checkoutDirectory !== currentConfig.buildOptions?.checkoutDirectory) {
1259
+ optionChanges["checkoutDirectory"] = {
1260
+ before: currentConfig.buildOptions?.checkoutDirectory ?? "",
1261
+ after: updates.buildOptions.checkoutDirectory
1262
+ };
1263
+ }
1264
+ if (Object.keys(optionChanges).length > 0) {
1265
+ changeLog["buildOptions"] = optionChanges;
1266
+ }
1267
+ }
1268
+ return changeLog;
1269
+ }
1270
+ /**
1271
+ * Rollback changes in case of failure
1272
+ */
1273
+ async rollbackChanges(configId, originalConfig) {
1274
+ try {
1275
+ info("Rolling back configuration changes", { configId });
1276
+ await this.applyUpdates(originalConfig, {
1277
+ name: originalConfig.name,
1278
+ description: originalConfig.description,
1279
+ buildNumberFormat: originalConfig.buildNumberFormat,
1280
+ artifactRules: originalConfig.artifactRules,
1281
+ parameters: originalConfig.parameters
1282
+ });
1283
+ info("Rollback completed successfully", { configId });
1284
+ } catch (err) {
1285
+ error("Failed to rollback changes", err);
1286
+ throw new Error("Rollback failed: Manual intervention may be required");
1287
+ }
1288
+ }
1289
+ /**
1290
+ * Validate parameter name according to TeamCity rules
1291
+ */
1292
+ isValidParameterName(name) {
1293
+ return /^[a-zA-Z0-9._-]+$/.test(name);
1294
+ }
1295
+ /**
1296
+ * Validate build number format
1297
+ */
1298
+ isValidBuildNumberFormat(format) {
1299
+ return format.includes("%") && (format.includes("build.counter") || format.includes("build.vcs.number") || format.includes("build.number"));
1300
+ }
1301
+ /**
1302
+ * Validate artifact rules
1303
+ */
1304
+ isValidArtifactRules(rules) {
1305
+ return rules.length > 0 && !rules.includes("\\\\");
1306
+ }
1307
+ };
1308
+
927
1309
  // src/teamcity/build-results-manager.ts
928
1310
  var import_axios = __toESM(require("axios"));
929
1311
  var BuildResultsManager = class _BuildResultsManager {
@@ -26223,15 +26605,57 @@ var FULL_MODE_TOOLS = [
26223
26605
  handler: async (args) => {
26224
26606
  const typedArgs = args;
26225
26607
  const api = TeamCityAPI.getInstance();
26226
- if (typedArgs.name != null && typedArgs.name !== "") {
26227
- await api.buildTypes.setBuildTypeField(typedArgs.buildTypeId, "name", typedArgs.name);
26228
- }
26229
- if (typedArgs.description !== void 0) {
26230
- await api.buildTypes.setBuildTypeField(
26231
- typedArgs.buildTypeId,
26232
- "description",
26233
- typedArgs.description
26234
- );
26608
+ try {
26609
+ const clientLike = { buildTypes: api.buildTypes };
26610
+ const manager = new BuildConfigurationUpdateManager(clientLike);
26611
+ const current = await manager.retrieveConfiguration(typedArgs.buildTypeId);
26612
+ if (current) {
26613
+ const updates = {};
26614
+ if (typedArgs.name != null && typedArgs.name !== "") updates.name = typedArgs.name;
26615
+ if (typedArgs.description !== void 0) updates.description = typedArgs.description;
26616
+ if (typedArgs.artifactRules !== void 0)
26617
+ updates.artifactRules = typedArgs.artifactRules;
26618
+ if (Object.keys(updates).length > 0) {
26619
+ await manager.validateUpdates(current, updates);
26620
+ await manager.applyUpdates(current, updates);
26621
+ }
26622
+ } else {
26623
+ if (typedArgs.name != null && typedArgs.name !== "") {
26624
+ await api.buildTypes.setBuildTypeField(typedArgs.buildTypeId, "name", typedArgs.name);
26625
+ }
26626
+ if (typedArgs.description !== void 0) {
26627
+ await api.buildTypes.setBuildTypeField(
26628
+ typedArgs.buildTypeId,
26629
+ "description",
26630
+ typedArgs.description
26631
+ );
26632
+ }
26633
+ if (typedArgs.artifactRules !== void 0) {
26634
+ await api.buildTypes.setBuildTypeField(
26635
+ typedArgs.buildTypeId,
26636
+ "settings/artifactRules",
26637
+ typedArgs.artifactRules
26638
+ );
26639
+ }
26640
+ }
26641
+ } catch {
26642
+ if (typedArgs.name != null && typedArgs.name !== "") {
26643
+ await api.buildTypes.setBuildTypeField(typedArgs.buildTypeId, "name", typedArgs.name);
26644
+ }
26645
+ if (typedArgs.description !== void 0) {
26646
+ await api.buildTypes.setBuildTypeField(
26647
+ typedArgs.buildTypeId,
26648
+ "description",
26649
+ typedArgs.description
26650
+ );
26651
+ }
26652
+ if (typedArgs.artifactRules !== void 0) {
26653
+ await api.buildTypes.setBuildTypeField(
26654
+ typedArgs.buildTypeId,
26655
+ "settings/artifactRules",
26656
+ typedArgs.artifactRules
26657
+ );
26658
+ }
26235
26659
  }
26236
26660
  if (typedArgs.paused !== void 0) {
26237
26661
  await api.buildTypes.setBuildTypeField(
@@ -26240,13 +26664,6 @@ var FULL_MODE_TOOLS = [
26240
26664
  String(typedArgs.paused)
26241
26665
  );
26242
26666
  }
26243
- if (typedArgs.artifactRules !== void 0) {
26244
- await api.buildTypes.setBuildTypeField(
26245
- typedArgs.buildTypeId,
26246
- "artifactRules",
26247
- typedArgs.artifactRules
26248
- );
26249
- }
26250
26667
  return json({ success: true, action: "update_build_config", id: typedArgs.buildTypeId });
26251
26668
  },
26252
26669
  mode: "full"