@daghis/teamcity-mcp 2.12.5 → 2.12.7

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
+ ## [2.12.7](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v2.12.6...teamcity-mcp-v2.12.7) (2026-10-03)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **build-log:** protect errors and bound stream snapshots ([#538](https://github.com/Daghis/teamcity-mcp/issues/538)) ([36a300c](https://github.com/Daghis/teamcity-mcp/commit/36a300c932fadeea6136effde9c93d82ad716780))
9
+
10
+ ## [2.12.6](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v2.12.5...teamcity-mcp-v2.12.6) (2026-10-03)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **build-log:** repair streamed downloads and error logging ([#526](https://github.com/Daghis/teamcity-mcp/issues/526)) ([aca2eea](https://github.com/Daghis/teamcity-mcp/commit/aca2eea6b996668e75ed69318c8846cdfeee624d))
16
+
3
17
  ## [2.12.5](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v2.12.4...teamcity-mcp-v2.12.5) (2026-10-03)
4
18
 
5
19
 
package/dist/index.js CHANGED
@@ -42,6 +42,71 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
42
42
  mod
43
43
  ));
44
44
 
45
+ // src/teamcity/utils/stream.ts
46
+ async function* sliceLines(source, startLine, endLine) {
47
+ if (endLine !== void 0 && endLine <= startLine) {
48
+ return;
49
+ }
50
+ let line = 0;
51
+ for await (const raw of source) {
52
+ const chunk = toBuffer(raw);
53
+ let pos = 0;
54
+ while (pos < chunk.length) {
55
+ const newline = chunk.indexOf(10, pos);
56
+ const stop = newline === -1 ? chunk.length : newline + 1;
57
+ if (line >= startLine) {
58
+ yield chunk.subarray(pos, stop);
59
+ }
60
+ if (newline === -1) {
61
+ break;
62
+ }
63
+ line += 1;
64
+ pos = stop;
65
+ if (endLine !== void 0 && line >= endLine) {
66
+ return;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ var import_stream, isReadableStream, destroyStream, discardStreamBody, toBuffer, sliceStreamLines;
72
+ var init_stream = __esm({
73
+ "src/teamcity/utils/stream.ts"() {
74
+ "use strict";
75
+ import_stream = require("stream");
76
+ isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function" && typeof value.on === "function";
77
+ destroyStream = (stream) => {
78
+ stream.on("error", () => void 0);
79
+ const destroyable = stream;
80
+ if (typeof destroyable.destroy === "function") {
81
+ destroyable.destroy();
82
+ } else {
83
+ stream.resume();
84
+ }
85
+ };
86
+ discardStreamBody = (value) => {
87
+ if (isReadableStream(value)) {
88
+ destroyStream(value);
89
+ }
90
+ };
91
+ toBuffer = (chunk) => {
92
+ if (Buffer.isBuffer(chunk)) {
93
+ return chunk;
94
+ }
95
+ return chunk instanceof Uint8Array ? Buffer.from(chunk) : Buffer.from(String(chunk));
96
+ };
97
+ sliceStreamLines = (source, startLine, lineCount) => {
98
+ const endLine = lineCount === void 0 ? void 0 : startLine + lineCount;
99
+ const slice = import_stream.Readable.from(sliceLines(source, startLine, endLine), { objectMode: false });
100
+ const destroySlice = slice._destroy.bind(slice);
101
+ slice._destroy = (error3, callback) => {
102
+ destroyStream(source);
103
+ destroySlice(error3, callback);
104
+ };
105
+ return slice;
106
+ };
107
+ }
108
+ });
109
+
45
110
  // src/teamcity/errors.ts
46
111
  function isRetryableError(error3) {
47
112
  if (error3 instanceof TeamCityAPIError) {
@@ -63,9 +128,11 @@ function isRetryableError(error3) {
63
128
  }
64
129
  return false;
65
130
  }
66
- var TeamCityAPIError, TeamCityAuthorizationError, TeamCityNotFoundError, TeamCityRateLimitError, TeamCityServerError, TeamCityNetworkError, TeamCityRequestError, TeamCityTimeoutError, BuildNotFoundError, BuildAccessDeniedError;
131
+ var import_node_util2, TeamCityAPIError, TeamCityAuthorizationError, TeamCityNotFoundError, TeamCityRateLimitError, TeamCityServerError, TeamCityNetworkError, TeamCityRequestError, TeamCityTimeoutError, BuildNotFoundError, BuildAccessDeniedError;
67
132
  var init_errors = __esm({
68
133
  "src/teamcity/errors.ts"() {
134
+ import_node_util2 = require("node:util");
135
+ init_stream();
69
136
  TeamCityAPIError = class _TeamCityAPIError extends Error {
70
137
  code;
71
138
  statusCode;
@@ -89,7 +156,7 @@ var init_errors = __esm({
89
156
  */
90
157
  static fromAxiosError(error3, requestId) {
91
158
  if (error3.response) {
92
- const data = error3.response.data;
159
+ const data = isReadableStream(error3.response.data) ? void 0 : error3.response.data;
93
160
  return new _TeamCityAPIError(
94
161
  (typeof data?.["message"] === "string" ? data["message"] : null) ?? error3.message,
95
162
  (typeof data?.["code"] === "string" ? data["code"] : null) ?? `HTTP_${error3.response.status}`,
@@ -115,6 +182,9 @@ var init_errors = __esm({
115
182
  stack: this.stack
116
183
  };
117
184
  }
185
+ [import_node_util2.inspect.custom]() {
186
+ return this.toJSON();
187
+ }
118
188
  };
119
189
  TeamCityAuthorizationError = class extends TeamCityAPIError {
120
190
  constructor(message = "Authorization failed", requestId, originalError) {
@@ -956,7 +1026,21 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
956
1026
 
957
1027
  // src/utils/logger/index.ts
958
1028
  var import_node_fs = require("node:fs");
1029
+ var import_node_util = require("node:util");
959
1030
  var import_winston = __toESM(require("winston"));
1031
+ function safeStringify(value) {
1032
+ try {
1033
+ return (0, import_node_util.inspect)(value, {
1034
+ depth: 4,
1035
+ maxArrayLength: 50,
1036
+ maxStringLength: 1e3,
1037
+ breakLength: Infinity,
1038
+ compact: true
1039
+ });
1040
+ } catch {
1041
+ return "[Uninspectable value]";
1042
+ }
1043
+ }
960
1044
  var TeamCityLogger = class _TeamCityLogger {
961
1045
  winston;
962
1046
  config;
@@ -1052,7 +1136,8 @@ var TeamCityLogger = class _TeamCityLogger {
1052
1136
  if (projectId) contextParts.push(`project=${projectId}`);
1053
1137
  if (duration !== void 0) contextParts.push(`${duration}ms`);
1054
1138
  const contextString = contextParts.length > 0 ? ` [${contextParts.join(" ")}]` : "";
1055
- const metaString = Object.keys(otherMeta).length > 0 ? ` ${JSON.stringify(otherMeta)}` : "";
1139
+ const metaEntries = Object.entries(otherMeta);
1140
+ const metaString = metaEntries.length > 0 ? ` ${safeStringify(Object.fromEntries(metaEntries))}` : "";
1056
1141
  return `${baseLog}${contextString}${metaString}`;
1057
1142
  }
1058
1143
  /**
@@ -1225,7 +1310,7 @@ function debug2(message, meta) {
1225
1310
  // package.json
1226
1311
  var package_default = {
1227
1312
  name: "@daghis/teamcity-mcp",
1228
- version: "2.12.5",
1313
+ version: "2.12.7",
1229
1314
  description: "Model Control Protocol server for TeamCity CI/CD integration with AI coding assistants",
1230
1315
  mcpName: "io.github.Daghis/teamcity",
1231
1316
  main: "dist/index.js",
@@ -4845,8 +4930,10 @@ axiosRetry.isRetryableError = isRetryableError2;
4845
4930
  var esm_default = axiosRetry;
4846
4931
 
4847
4932
  // src/teamcity/auth.ts
4933
+ var import_node_string_decoder = require("node:string_decoder");
4848
4934
  var import_crypto = require("crypto");
4849
4935
  init_errors();
4936
+ init_stream();
4850
4937
  var asTimingMetaContainer = (value) => {
4851
4938
  if (typeof value === "object" && value !== null) {
4852
4939
  return value;
@@ -4891,10 +4978,54 @@ function logResponse(response) {
4891
4978
  });
4892
4979
  return response;
4893
4980
  }
4894
- function logAndTransformError(error3) {
4981
+ var STREAM_BODY_TIMEOUT_MS = 5e3;
4982
+ var streamToString = async (stream, maxBytes = 64 * 1024) => {
4983
+ const chunks = [];
4984
+ let total = 0;
4985
+ let truncated = false;
4986
+ for await (const chunk of stream) {
4987
+ const buf = toBuffer(chunk);
4988
+ const retained = buf.subarray(0, maxBytes - total);
4989
+ chunks.push(retained);
4990
+ total += retained.length;
4991
+ if (total >= maxBytes) {
4992
+ truncated = true;
4993
+ break;
4994
+ }
4995
+ }
4996
+ const decoder = new import_node_string_decoder.StringDecoder("utf8");
4997
+ const text = decoder.write(Buffer.concat(chunks, total)) + (truncated ? "" : decoder.end());
4998
+ if (Buffer.byteLength(text) <= maxBytes) {
4999
+ return text;
5000
+ }
5001
+ return new import_node_string_decoder.StringDecoder("utf8").write(Buffer.from(text).subarray(0, maxBytes));
5002
+ };
5003
+ var snapshotStreamBody = async (stream, timeoutMs) => {
5004
+ let timer;
5005
+ const deadline = new Promise((_resolve, reject) => {
5006
+ timer = setTimeout(() => reject(new Error("Timed out reading streamed error body")), timeoutMs);
5007
+ });
5008
+ try {
5009
+ return await Promise.race([streamToString(stream), deadline]);
5010
+ } catch {
5011
+ destroyStream(stream);
5012
+ return void 0;
5013
+ } finally {
5014
+ clearTimeout(timer);
5015
+ }
5016
+ };
5017
+ async function logAndTransformError(error3) {
4895
5018
  if (error3 instanceof TeamCityAPIError) {
4896
5019
  return Promise.reject(error3);
4897
5020
  }
5021
+ const response = error3.response;
5022
+ if (response && isReadableStream(response.data)) {
5023
+ const requestTimeout = error3.config?.timeout ?? 0;
5024
+ response.data = await snapshotStreamBody(
5025
+ response.data,
5026
+ requestTimeout > 0 ? Math.min(requestTimeout, STREAM_BODY_TIMEOUT_MS) : STREAM_BODY_TIMEOUT_MS
5027
+ );
5028
+ }
4898
5029
  const requestId = error3.config?.requestId;
4899
5030
  const tcError = TeamCityAPIError.fromAxiosError(error3, requestId);
4900
5031
  const meta = asTimingMetaContainer(error3.config)?._tcMeta;
@@ -4950,6 +5081,7 @@ function validateConfiguration(baseUrl, token) {
4950
5081
 
4951
5082
  // src/api-client.ts
4952
5083
  init_errors();
5084
+ init_stream();
4953
5085
 
4954
5086
  // src/teamcity-client/api/agent-api.ts
4955
5087
  var import_axios5 = __toESM(require("axios"));
@@ -38427,6 +38559,24 @@ var extractRetryAfterMilliseconds = (value) => {
38427
38559
  }
38428
38560
  return void 0;
38429
38561
  };
38562
+ var toOptionalLineNumber = (value) => {
38563
+ if (value === void 0 || value === null || value === "") {
38564
+ return void 0;
38565
+ }
38566
+ const parsed = Number(value);
38567
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
38568
+ };
38569
+ var sliceLogData = (data, startLine, lineCount) => {
38570
+ if (isReadableStream(data)) {
38571
+ return sliceStreamLines(data, startLine, lineCount);
38572
+ }
38573
+ if (typeof data === "string") {
38574
+ const lines = data.split("\n");
38575
+ const end = lineCount === void 0 ? void 0 : startLine + lineCount;
38576
+ return lines.slice(startLine, end).join("\n");
38577
+ }
38578
+ return data;
38579
+ };
38430
38580
  var TeamCityAPI = class _TeamCityAPI {
38431
38581
  static instance;
38432
38582
  static instanceConfig;
@@ -38491,6 +38641,7 @@ var TeamCityAPI = class _TeamCityAPI {
38491
38641
  this.http = this.axiosInstance;
38492
38642
  esm_default(this.axiosInstance, {
38493
38643
  retries: 3,
38644
+ onRetry: (_retryCount, error3) => discardStreamBody(error3.response?.data),
38494
38645
  retryDelay: (retryCount, error3) => {
38495
38646
  const reqId = error3?.config?.requestId;
38496
38647
  const tcError = TeamCityAPIError.fromAxiosError(error3, reqId);
@@ -38748,27 +38899,51 @@ var TeamCityAPI = class _TeamCityAPI {
38748
38899
  );
38749
38900
  }
38750
38901
  async downloadBuildLog(buildId, options) {
38902
+ const responseType = options?.responseType ?? "text";
38903
+ const rawHeaders = options?.headers ?? void 0;
38904
+ const headers = { Accept: "text/plain", ...rawHeaders ?? {} };
38905
+ const transformResponse = options?.transformResponse ?? [(data) => data];
38751
38906
  const rawParams = options?.params ?? void 0;
38752
- const params = rawParams ? { ...rawParams } : {};
38753
- if (!Object.prototype.hasOwnProperty.call(params, "plain")) {
38754
- params["plain"] = true;
38907
+ const { start, count, ...primaryParams } = rawParams ?? {};
38908
+ const startLine = toOptionalLineNumber(start);
38909
+ const lineCount = toOptionalLineNumber(count);
38910
+ try {
38911
+ const response = await this.axiosInstance.get(`/downloadBuildLog.html`, {
38912
+ ...options,
38913
+ params: { ...primaryParams, buildId },
38914
+ headers,
38915
+ responseType,
38916
+ transformResponse
38917
+ });
38918
+ if (startLine === void 0 && lineCount === void 0) {
38919
+ return response;
38920
+ }
38921
+ return { ...response, data: sliceLogData(response.data, startLine ?? 0, lineCount) };
38922
+ } catch (primaryError) {
38923
+ const status = primaryError instanceof TeamCityAPIError ? primaryError.statusCode : void 0;
38924
+ if (status === void 0 || [401, 403, 429, 503].includes(status)) {
38925
+ throw primaryError;
38926
+ }
38927
+ const params = rawParams ? { ...rawParams } : {};
38928
+ if (!Object.prototype.hasOwnProperty.call(params, "plain")) {
38929
+ params["plain"] = true;
38930
+ }
38931
+ try {
38932
+ return await this.axiosInstance.get(`/app/rest/builds/${toBuildLocator(buildId)}/log`, {
38933
+ ...options,
38934
+ params,
38935
+ headers,
38936
+ responseType,
38937
+ transformResponse
38938
+ });
38939
+ } catch (fallbackError) {
38940
+ const fallbackStatus = fallbackError instanceof TeamCityAPIError ? fallbackError.statusCode : void 0;
38941
+ if (fallbackStatus === 404 || fallbackStatus === 405) {
38942
+ throw primaryError;
38943
+ }
38944
+ throw fallbackError;
38945
+ }
38755
38946
  }
38756
- const rawHeaders = options?.headers ?? void 0;
38757
- const headers = rawHeaders ? { ...rawHeaders } : {};
38758
- const requestOptions = {
38759
- ...options,
38760
- params,
38761
- headers: {
38762
- Accept: "text/plain",
38763
- ...headers
38764
- },
38765
- responseType: options?.responseType ?? "text",
38766
- transformResponse: options?.transformResponse ?? [(data) => data]
38767
- };
38768
- return this.axiosInstance.get(
38769
- `/app/rest/builds/${toBuildLocator(buildId)}/log`,
38770
- requestOptions
38771
- );
38772
38947
  }
38773
38948
  async getBuildStatistics(buildId, fields) {
38774
38949
  return this.builds.getBuildStatisticValues(toBuildLocator(buildId), fields);
@@ -38857,7 +39032,7 @@ var extraHeadersEqual = (a, b) => {
38857
39032
  };
38858
39033
 
38859
39034
  // src/tools.ts
38860
- var isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
39035
+ var isReadableStream2 = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
38861
39036
  var isAxios4042 = (error3) => (0, import_axios36.isAxiosError)(error3) && error3.response?.status === 404;
38862
39037
  var buildIdentifierSchema = import_zod4.z.object({
38863
39038
  buildId: import_zod4.z.string().min(1).optional(),
@@ -38982,7 +39157,7 @@ var writeArtifactStreamToDisk = async (artifact, stream, options) => {
38982
39157
  var buildArtifactPayload = async (artifact, encoding, options) => {
38983
39158
  if (encoding === "stream") {
38984
39159
  const contentStream = artifact.content;
38985
- if (!isReadableStream(contentStream)) {
39160
+ if (!isReadableStream2(contentStream)) {
38986
39161
  throw new Error("Streaming download did not return a readable stream");
38987
39162
  }
38988
39163
  const { outputPath, bytesWritten } = await writeArtifactStreamToDisk(
@@ -39066,7 +39241,7 @@ var downloadArtifactByUrl = async (adapter, request, encoding, options) => {
39066
39241
  }
39067
39242
  if (encoding === "stream") {
39068
39243
  const stream = response.data;
39069
- if (!isReadableStream(stream)) {
39244
+ if (!isReadableStream2(stream)) {
39070
39245
  throw new Error("Streaming download did not return a readable stream");
39071
39246
  }
39072
39247
  const artifact2 = {
@@ -40237,7 +40412,7 @@ var DEV_TOOLS = [
40237
40412
  responseType: "stream"
40238
40413
  });
40239
40414
  const stream = response.data;
40240
- if (!isReadableStream(stream)) {
40415
+ if (!isReadableStream2(stream)) {
40241
40416
  throw new Error("Streaming log download did not return a readable stream");
40242
40417
  }
40243
40418
  const safeBuildId = effectiveBuildId.replace(/[^a-zA-Z0-9._-]/g, "_") || "build";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daghis/teamcity-mcp",
3
- "version": "2.12.5",
3
+ "version": "2.12.7",
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",
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": "2.12.5",
10
+ "version": "2.12.7",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@daghis/teamcity-mcp",
16
- "version": "2.12.5",
16
+ "version": "2.12.7",
17
17
  "runtimeHint": "npx",
18
18
  "runtimeArguments": [
19
19
  {