@daghis/teamcity-mcp 1.12.0 → 1.13.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.13.0](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v1.12.1...teamcity-mcp-v1.13.0) (2025-12-22)
4
+
5
+
6
+ ### Features
7
+
8
+ * add CLI argument support for Windows workaround ([#320](https://github.com/Daghis/teamcity-mcp/issues/320)) ([#326](https://github.com/Daghis/teamcity-mcp/issues/326)) ([cc05a4d](https://github.com/Daghis/teamcity-mcp/commit/cc05a4dd19c1d7e1f547cdd35bb910035141909d))
9
+
10
+ ## [1.12.1](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v1.12.0...teamcity-mcp-v1.12.1) (2025-12-22)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * handle queued builds in get_build and get_build_status ([#324](https://github.com/Daghis/teamcity-mcp/issues/324)) ([4cb2dab](https://github.com/Daghis/teamcity-mcp/commit/4cb2dabfc9161e07708a899622d78b886742459d))
16
+
3
17
  ## [1.12.0](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v1.11.20...teamcity-mcp-v1.12.0) (2025-12-06)
4
18
 
5
19
 
package/README.md CHANGED
@@ -85,10 +85,40 @@ npx -y @daghis/teamcity-mcp
85
85
  - `claude mcp add [-s user] teamcity -- npx -y @daghis/teamcity-mcp`
86
86
  - With env vars (if not using .env):
87
87
  - `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`
88
+ - With CLI arguments (recommended for Windows):
89
+ - `claude mcp add [-s user] teamcity -- npx -y @daghis/teamcity-mcp --url "https://teamcity.example.com" --token "tc_<your_token>" --mode dev`
88
90
  - Context usage (Opus 4.1, estimates):
89
91
  - Dev (default): ~14k tokens for MCP tools
90
92
  - Full (`MCP_MODE=full`): ~26k tokens for MCP tools
91
93
 
94
+ ### Windows Users
95
+
96
+ On Windows, Claude Code's MCP configuration may not properly merge environment variables. Use CLI arguments as a workaround:
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "teamcity": {
102
+ "command": "npx",
103
+ "args": ["-y", "@daghis/teamcity-mcp", "--url", "https://teamcity.example.com", "--token", "YOUR_TOKEN"]
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ Or use a config file for better security (token not visible in process list):
110
+
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "teamcity": {
115
+ "command": "npx",
116
+ "args": ["-y", "@daghis/teamcity-mcp", "--config", "C:\\path\\to\\teamcity.env"]
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
92
122
  ## Configuration
93
123
 
94
124
  Environment is validated centrally with Zod. Supported variables and defaults:
package/dist/index.js CHANGED
@@ -221,6 +221,12 @@ var build_status_manager_exports = {};
221
221
  __export(build_status_manager_exports, {
222
222
  BuildStatusManager: () => BuildStatusManager
223
223
  });
224
+ function isAxios404(error3) {
225
+ return error3 != null && typeof error3 === "object" && "response" in error3 && error3.response?.status === 404;
226
+ }
227
+ function isAxios403(error3) {
228
+ return error3 != null && typeof error3 === "object" && "response" in error3 && error3.response?.status === 403;
229
+ }
224
230
  var BuildStatusManager;
225
231
  var init_build_status_manager = __esm({
226
232
  "src/teamcity/build-status-manager.ts"() {
@@ -235,7 +241,11 @@ var init_build_status_manager = __esm({
235
241
  this.cache = /* @__PURE__ */ new Map();
236
242
  }
237
243
  /**
238
- * Get build status by ID or number
244
+ * Get build status by ID or number.
245
+ * Uses 3-step fallback to handle builds in queue and race conditions:
246
+ * 1. Try builds endpoint
247
+ * 2. On 404, try build queue (build may be queued)
248
+ * 3. On 404 again, retry builds endpoint (build may have left queue between checks)
239
249
  */
240
250
  async getBuildStatus(options) {
241
251
  if (!options.buildId && !options.buildNumber) {
@@ -252,41 +262,93 @@ var init_build_status_manager = __esm({
252
262
  }
253
263
  }
254
264
  try {
255
- let buildData;
256
- if (options.buildId) {
257
- const response = await this.client.builds.getBuild(
258
- `id:${options.buildId}`,
259
- this.getFieldSelection(options)
260
- );
261
- buildData = response.data;
262
- } else {
263
- const locator = this.buildLocator(options);
264
- const response = await this.client.builds.getBuild(
265
- locator,
266
- this.getFieldSelection(options)
267
- );
268
- buildData = response.data;
269
- }
270
- if (buildData == null) {
271
- throw new BuildNotFoundError("Build data is undefined");
272
- }
273
- const result = this.transformBuildResponse(buildData, options);
274
- if (result.state === "finished" || result.state === "canceled") {
275
- this.setCachedResult(cacheKey, result);
276
- }
277
- return result;
265
+ return await this.getBuildStatusFromBuildsEndpoint(options, cacheKey);
278
266
  } catch (error3) {
279
- if (error3 != null && typeof error3 === "object" && "response" in error3 && error3.response?.status === 404) {
280
- throw new BuildNotFoundError(`Build not found: ${options.buildId ?? options.buildNumber}`);
267
+ if (!isAxios404(error3) || !options.buildId) {
268
+ this.handleBuildStatusError(error3, options);
281
269
  }
282
- if (error3 != null && typeof error3 === "object" && "response" in error3 && error3.response?.status === 403) {
283
- throw new BuildAccessDeniedError(
284
- `Access denied to build: ${options.buildId ?? options.buildNumber}`
285
- );
270
+ }
271
+ const buildId = options.buildId;
272
+ try {
273
+ return await this.getQueuedBuildStatus(buildId);
274
+ } catch (queueError) {
275
+ if (!isAxios404(queueError)) {
276
+ this.handleBuildStatusError(queueError, options);
286
277
  }
287
- throw error3;
278
+ }
279
+ try {
280
+ return await this.getBuildStatusFromBuildsEndpoint(options, cacheKey);
281
+ } catch (error3) {
282
+ this.handleBuildStatusError(error3, options);
288
283
  }
289
284
  }
285
+ /**
286
+ * Get build status from the builds endpoint
287
+ */
288
+ async getBuildStatusFromBuildsEndpoint(options, cacheKey) {
289
+ let buildData;
290
+ if (options.buildId) {
291
+ const response = await this.client.builds.getBuild(
292
+ `id:${options.buildId}`,
293
+ this.getFieldSelection(options)
294
+ );
295
+ buildData = response.data;
296
+ } else {
297
+ const locator = this.buildLocator(options);
298
+ const response = await this.client.builds.getBuild(locator, this.getFieldSelection(options));
299
+ buildData = response.data;
300
+ }
301
+ if (buildData == null) {
302
+ throw new BuildNotFoundError("Build data is undefined");
303
+ }
304
+ const result = this.transformBuildResponse(buildData, options);
305
+ if (result.state === "finished" || result.state === "canceled") {
306
+ this.setCachedResult(cacheKey, result);
307
+ }
308
+ return result;
309
+ }
310
+ /**
311
+ * Get build status from the build queue
312
+ */
313
+ async getQueuedBuildStatus(buildId) {
314
+ const response = await this.client.modules.buildQueue.getQueuedBuild(
315
+ `id:${buildId}`,
316
+ "id,number,state,status,buildTypeId,branchName,webUrl,queuedDate,waitReason"
317
+ );
318
+ const queuedBuild = response.data;
319
+ if (queuedBuild == null) {
320
+ throw new BuildNotFoundError("Queued build data is undefined");
321
+ }
322
+ const result = {
323
+ buildId: String(queuedBuild.id),
324
+ buildNumber: queuedBuild.number,
325
+ buildTypeId: queuedBuild.buildTypeId,
326
+ state: "queued",
327
+ status: void 0,
328
+ percentageComplete: 0,
329
+ branchName: queuedBuild.branchName,
330
+ webUrl: queuedBuild.webUrl,
331
+ waitReason: queuedBuild.waitReason
332
+ };
333
+ if (queuedBuild.queuedDate) {
334
+ result.queuedDate = this.parseDate(queuedBuild.queuedDate);
335
+ }
336
+ return result;
337
+ }
338
+ /**
339
+ * Handle and re-throw build status errors with appropriate error types
340
+ */
341
+ handleBuildStatusError(error3, options) {
342
+ if (isAxios404(error3)) {
343
+ throw new BuildNotFoundError(`Build not found: ${options.buildId ?? options.buildNumber}`);
344
+ }
345
+ if (isAxios403(error3)) {
346
+ throw new BuildAccessDeniedError(
347
+ `Access denied to build: ${options.buildId ?? options.buildNumber}`
348
+ );
349
+ }
350
+ throw error3;
351
+ }
290
352
  /**
291
353
  * Get build status using custom locator
292
354
  */
@@ -679,6 +741,185 @@ async function startServerLifecycle(server, transport) {
679
741
  });
680
742
  }
681
743
 
744
+ // src/utils/cli-args.ts
745
+ var import_fs = require("fs");
746
+ var import_path = require("path");
747
+ function parseCliArgs(argv) {
748
+ const result = {
749
+ help: false,
750
+ version: false
751
+ };
752
+ for (let i = 0; i < argv.length; i++) {
753
+ const arg = argv[i];
754
+ if (arg === void 0) continue;
755
+ if (arg === "--help" || arg === "-h") {
756
+ result.help = true;
757
+ continue;
758
+ }
759
+ if (arg === "--version" || arg === "-v") {
760
+ result.version = true;
761
+ continue;
762
+ }
763
+ if (arg.startsWith("--url=")) {
764
+ result.url = arg.slice("--url=".length);
765
+ continue;
766
+ }
767
+ if (arg.startsWith("--token=")) {
768
+ result.token = arg.slice("--token=".length);
769
+ continue;
770
+ }
771
+ if (arg.startsWith("--mode=")) {
772
+ const mode = arg.slice("--mode=".length);
773
+ if (mode === "dev" || mode === "full") {
774
+ result.mode = mode;
775
+ } else if (mode.length > 0) {
776
+ process.stderr.write(
777
+ `Warning: Invalid mode '${mode}'. Valid values are 'dev' or 'full'.
778
+ `
779
+ );
780
+ }
781
+ continue;
782
+ }
783
+ if (arg.startsWith("--config=")) {
784
+ result.config = arg.slice("--config=".length);
785
+ continue;
786
+ }
787
+ const nextArg = argv[i + 1];
788
+ if (nextArg !== void 0 && !nextArg.startsWith("-")) {
789
+ if (arg === "--url") {
790
+ result.url = nextArg;
791
+ i++;
792
+ continue;
793
+ }
794
+ if (arg === "--token") {
795
+ result.token = nextArg;
796
+ i++;
797
+ continue;
798
+ }
799
+ if (arg === "--mode") {
800
+ if (nextArg === "dev" || nextArg === "full") {
801
+ result.mode = nextArg;
802
+ } else {
803
+ process.stderr.write(
804
+ `Warning: Invalid mode '${nextArg}'. Valid values are 'dev' or 'full'.
805
+ `
806
+ );
807
+ }
808
+ i++;
809
+ continue;
810
+ }
811
+ if (arg === "--config") {
812
+ result.config = nextArg;
813
+ i++;
814
+ continue;
815
+ }
816
+ }
817
+ }
818
+ return result;
819
+ }
820
+ function getVersion() {
821
+ try {
822
+ const possiblePaths = [
823
+ (0, import_path.join)(__dirname, "../package.json"),
824
+ // bundled: dist/ -> package.json
825
+ (0, import_path.join)(__dirname, "../../package.json")
826
+ // source: src/utils/ -> package.json
827
+ ];
828
+ for (const packagePath of possiblePaths) {
829
+ try {
830
+ const packageJson = JSON.parse((0, import_fs.readFileSync)(packagePath, "utf-8"));
831
+ if (packageJson.version) {
832
+ return packageJson.version;
833
+ }
834
+ } catch {
835
+ }
836
+ }
837
+ return "unknown";
838
+ } catch {
839
+ return "unknown";
840
+ }
841
+ }
842
+ function getHelpText() {
843
+ const version = getVersion();
844
+ return `teamcity-mcp v${version}
845
+ Model Context Protocol server for TeamCity CI/CD integration
846
+
847
+ USAGE:
848
+ teamcity-mcp [OPTIONS]
849
+
850
+ OPTIONS:
851
+ --url <url> TeamCity server URL (e.g., https://tc.example.com)
852
+ --token <token> TeamCity API token for authentication
853
+ --mode <dev|full> Tool exposure mode: dev (limited) or full (all tools)
854
+ --config <path> Path to .env format configuration file
855
+
856
+ -h, --help Show this help message
857
+ -v, --version Show version number
858
+
859
+ CONFIGURATION PRECEDENCE (highest to lowest):
860
+ 1. CLI arguments (--url, --token, --mode)
861
+ 2. Config file (--config)
862
+ 3. Environment variables (TEAMCITY_URL, TEAMCITY_TOKEN, MCP_MODE)
863
+ 4. .env file in current directory
864
+
865
+ SECURITY WARNING:
866
+ Avoid using --token on the command line when possible. The token value
867
+ is visible in process lists and may be logged in shell history. For
868
+ production use, prefer environment variables or a config file with
869
+ restricted permissions (chmod 600).
870
+
871
+ EXAMPLES:
872
+ # Using CLI arguments
873
+ teamcity-mcp --url https://tc.example.com --token tc_abc123
874
+
875
+ # Using a config file
876
+ teamcity-mcp --config /path/to/teamcity.env
877
+
878
+ # Override config file with CLI arg
879
+ teamcity-mcp --config prod.env --mode dev
880
+
881
+ CONFIG FILE FORMAT (.env):
882
+ TEAMCITY_URL=https://tc.example.com
883
+ TEAMCITY_TOKEN=tc_abc123
884
+ MCP_MODE=dev
885
+
886
+ For more information, visit: https://github.com/Daghis/teamcity-mcp
887
+ `;
888
+ }
889
+
890
+ // src/utils/env-file.ts
891
+ var import_dotenv2 = require("dotenv");
892
+ var import_fs2 = require("fs");
893
+ function loadEnvFile(filepath) {
894
+ try {
895
+ const content = (0, import_fs2.readFileSync)(filepath, "utf-8");
896
+ const values = (0, import_dotenv2.parse)(content);
897
+ return {
898
+ success: true,
899
+ values
900
+ };
901
+ } catch (err) {
902
+ const error3 = err instanceof Error ? err : new Error(String(err));
903
+ const errno = err;
904
+ if (errno.code === "ENOENT") {
905
+ return {
906
+ success: false,
907
+ error: `Config file not found: ${filepath}`
908
+ };
909
+ }
910
+ if (errno.code === "EACCES") {
911
+ return {
912
+ success: false,
913
+ error: `Permission denied reading config file: ${filepath}`
914
+ };
915
+ }
916
+ return {
917
+ success: false,
918
+ error: `Failed to read config file: ${error3.message}`
919
+ };
920
+ }
921
+ }
922
+
682
923
  // src/server.ts
683
924
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
684
925
  var import_types = require("@modelcontextprotocol/sdk/types.js");
@@ -954,7 +1195,7 @@ function debug2(message, meta) {
954
1195
  // package.json
955
1196
  var package_default = {
956
1197
  name: "@daghis/teamcity-mcp",
957
- version: "1.12.0",
1198
+ version: "1.13.0",
958
1199
  description: "Model Control Protocol server for TeamCity CI/CD integration with AI coding assistants",
959
1200
  mcpName: "io.github.Daghis/teamcity",
960
1201
  main: "dist/index.js",
@@ -1040,7 +1281,7 @@ var package_default = {
1040
1281
  "@types/jest": "^30.0.0",
1041
1282
  "@types/js-yaml": "^4.0.9",
1042
1283
  "@types/morgan": "^1.9.9",
1043
- "@types/node": "^24.10.0",
1284
+ "@types/node": "^25.0.2",
1044
1285
  "@typescript-eslint/eslint-plugin": "^8.46.3",
1045
1286
  "@typescript-eslint/parser": "^8.46.3",
1046
1287
  "axios-retry": "^4.5.0",
@@ -38578,6 +38819,7 @@ var TeamCityAPI = class _TeamCityAPI {
38578
38819
 
38579
38820
  // src/tools.ts
38580
38821
  var isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
38822
+ var isAxios4042 = (error3) => (0, import_axios36.isAxiosError)(error3) && error3.response?.status === 404;
38581
38823
  var sanitizeFileName = (artifactName) => {
38582
38824
  const base = (0, import_node_path.basename)(artifactName || "artifact");
38583
38825
  const safeBase = base.replace(/[^a-zA-Z0-9._-]/g, "_") || "artifact";
@@ -39001,7 +39243,7 @@ var DEV_TOOLS = [
39001
39243
  },
39002
39244
  {
39003
39245
  name: "get_build",
39004
- description: "Get details of a specific build",
39246
+ description: "Get details of a specific build (works for both queued and running/finished builds)",
39005
39247
  inputSchema: {
39006
39248
  type: "object",
39007
39249
  properties: {
@@ -39016,6 +39258,18 @@ var DEV_TOOLS = [
39016
39258
  schema,
39017
39259
  async (typed) => {
39018
39260
  const adapter = createAdapterFromTeamCityAPI(TeamCityAPI.getInstance());
39261
+ try {
39262
+ const build2 = await adapter.getBuild(typed.buildId);
39263
+ return json(build2);
39264
+ } catch (error3) {
39265
+ if (!isAxios4042(error3)) throw error3;
39266
+ }
39267
+ try {
39268
+ const qb = await adapter.modules.buildQueue.getQueuedBuild(`id:${typed.buildId}`);
39269
+ return json({ ...qb.data, state: "queued" });
39270
+ } catch (queueError) {
39271
+ if (!isAxios4042(queueError)) throw queueError;
39272
+ }
39019
39273
  const build = await adapter.getBuild(typed.buildId);
39020
39274
  return json(build);
39021
39275
  },
@@ -39233,21 +39487,19 @@ var DEV_TOOLS = [
39233
39487
  if (typeof result.queuePosition === "number") {
39234
39488
  enrich.canMoveToTop = result.queuePosition > 1;
39235
39489
  }
39236
- if (typed.includeQueueTotals) {
39237
- try {
39238
- const countResp = await adapter.modules.buildQueue.getAllQueuedBuilds(
39239
- void 0,
39240
- "count"
39241
- );
39242
- enrich.totalQueued = countResp.data.count;
39243
- } catch {
39244
- }
39490
+ try {
39491
+ const countResp = await adapter.modules.buildQueue.getAllQueuedBuilds(
39492
+ void 0,
39493
+ "count"
39494
+ );
39495
+ enrich.totalQueued = countResp.data.count;
39496
+ } catch {
39245
39497
  }
39246
- if (typed.includeQueueReason) {
39498
+ if (!result.waitReason) {
39247
39499
  try {
39248
39500
  const targetBuildId = typed.buildId ?? result.buildId;
39249
39501
  if (targetBuildId) {
39250
- const qb = await adapter.modules.buildQueue.getQueuedBuild(targetBuildId);
39502
+ const qb = await adapter.modules.buildQueue.getQueuedBuild(`id:${targetBuildId}`);
39251
39503
  enrich.waitReason = qb.data.waitReason;
39252
39504
  }
39253
39505
  } catch {
@@ -43316,7 +43568,39 @@ function createSimpleServer() {
43316
43568
  }
43317
43569
 
43318
43570
  // src/index.ts
43571
+ var cliArgs = parseCliArgs(process.argv.slice(2));
43572
+ if (cliArgs.help) {
43573
+ process.stderr.write(getHelpText());
43574
+ process.exit(0);
43575
+ }
43576
+ if (cliArgs.version) {
43577
+ process.stderr.write(`teamcity-mcp v${getVersion()}
43578
+ `);
43579
+ process.exit(0);
43580
+ }
43581
+ if (cliArgs.config) {
43582
+ const configResult = loadEnvFile(cliArgs.config);
43583
+ if (!configResult.success) {
43584
+ process.stderr.write(`Error: ${configResult.error}
43585
+ `);
43586
+ process.exit(1);
43587
+ }
43588
+ if (configResult.values) {
43589
+ for (const [key, value] of Object.entries(configResult.values)) {
43590
+ process.env[key] ||= value;
43591
+ }
43592
+ }
43593
+ }
43319
43594
  dotenv2.config({ quiet: true });
43595
+ if (cliArgs.url) {
43596
+ process.env["TEAMCITY_URL"] = cliArgs.url;
43597
+ }
43598
+ if (cliArgs.token) {
43599
+ process.env["TEAMCITY_TOKEN"] = cliArgs.token;
43600
+ }
43601
+ if (cliArgs.mode) {
43602
+ process.env["MCP_MODE"] = cliArgs.mode;
43603
+ }
43320
43604
  var activeServer = null;
43321
43605
  var lifecyclePromise = null;
43322
43606
  var shuttingDown = false;
@@ -43355,7 +43639,7 @@ async function main() {
43355
43639
  process.stderr.write(`${e.message}
43356
43640
  `);
43357
43641
  process.stderr.write(
43358
- "Please set TEAMCITY_URL and TEAMCITY_TOKEN in your environment or .env file.\n"
43642
+ "Please configure TEAMCITY_URL and TEAMCITY_TOKEN via:\n - CLI arguments: --url <url> --token <token>\n - Config file: --config <path>\n - Environment variables\n - .env file\nRun with --help for more information.\n"
43359
43643
  );
43360
43644
  process.exit(1);
43361
43645
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daghis/teamcity-mcp",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Model Control Protocol server for TeamCity CI/CD integration with AI coding assistants",
5
5
  "mcpName": "io.github.Daghis/teamcity",
6
6
  "main": "dist/index.js",
@@ -86,7 +86,7 @@
86
86
  "@types/jest": "^30.0.0",
87
87
  "@types/js-yaml": "^4.0.9",
88
88
  "@types/morgan": "^1.9.9",
89
- "@types/node": "^24.10.0",
89
+ "@types/node": "^25.0.2",
90
90
  "@typescript-eslint/eslint-plugin": "^8.46.3",
91
91
  "@typescript-eslint/parser": "^8.46.3",
92
92
  "axios-retry": "^4.5.0",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://github.com/Daghis/teamcity-mcp",
10
- "version": "1.12.0",
10
+ "version": "1.13.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@daghis/teamcity-mcp",
16
- "version": "1.12.0",
16
+ "version": "1.13.0",
17
17
  "runtimeHint": "npx",
18
18
  "runtimeArguments": [
19
19
  {