@daghis/teamcity-mcp 2.12.5 → 2.12.6

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.12.6](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v2.12.5...teamcity-mcp-v2.12.6) (2026-10-03)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **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))
9
+
3
10
  ## [2.12.5](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v2.12.4...teamcity-mcp-v2.12.5) (2026-10-03)
4
11
 
5
12
 
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) {
@@ -66,6 +131,7 @@ function isRetryableError(error3) {
66
131
  var TeamCityAPIError, TeamCityAuthorizationError, TeamCityNotFoundError, TeamCityRateLimitError, TeamCityServerError, TeamCityNetworkError, TeamCityRequestError, TeamCityTimeoutError, BuildNotFoundError, BuildAccessDeniedError;
67
132
  var init_errors = __esm({
68
133
  "src/teamcity/errors.ts"() {
134
+ init_stream();
69
135
  TeamCityAPIError = class _TeamCityAPIError extends Error {
70
136
  code;
71
137
  statusCode;
@@ -89,7 +155,7 @@ var init_errors = __esm({
89
155
  */
90
156
  static fromAxiosError(error3, requestId) {
91
157
  if (error3.response) {
92
- const data = error3.response.data;
158
+ const data = isReadableStream(error3.response.data) ? void 0 : error3.response.data;
93
159
  return new _TeamCityAPIError(
94
160
  (typeof data?.["message"] === "string" ? data["message"] : null) ?? error3.message,
95
161
  (typeof data?.["code"] === "string" ? data["code"] : null) ?? `HTTP_${error3.response.status}`,
@@ -956,7 +1022,21 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
956
1022
 
957
1023
  // src/utils/logger/index.ts
958
1024
  var import_node_fs = require("node:fs");
1025
+ var import_node_util = require("node:util");
959
1026
  var import_winston = __toESM(require("winston"));
1027
+ function safeStringify(value) {
1028
+ try {
1029
+ return (0, import_node_util.inspect)(value, {
1030
+ depth: 4,
1031
+ maxArrayLength: 50,
1032
+ maxStringLength: 1e3,
1033
+ breakLength: Infinity,
1034
+ compact: true
1035
+ });
1036
+ } catch {
1037
+ return "[Uninspectable value]";
1038
+ }
1039
+ }
960
1040
  var TeamCityLogger = class _TeamCityLogger {
961
1041
  winston;
962
1042
  config;
@@ -1052,7 +1132,8 @@ var TeamCityLogger = class _TeamCityLogger {
1052
1132
  if (projectId) contextParts.push(`project=${projectId}`);
1053
1133
  if (duration !== void 0) contextParts.push(`${duration}ms`);
1054
1134
  const contextString = contextParts.length > 0 ? ` [${contextParts.join(" ")}]` : "";
1055
- const metaString = Object.keys(otherMeta).length > 0 ? ` ${JSON.stringify(otherMeta)}` : "";
1135
+ const metaEntries = Object.entries(otherMeta);
1136
+ const metaString = metaEntries.length > 0 ? ` ${safeStringify(Object.fromEntries(metaEntries))}` : "";
1056
1137
  return `${baseLog}${contextString}${metaString}`;
1057
1138
  }
1058
1139
  /**
@@ -1225,7 +1306,7 @@ function debug2(message, meta) {
1225
1306
  // package.json
1226
1307
  var package_default = {
1227
1308
  name: "@daghis/teamcity-mcp",
1228
- version: "2.12.5",
1309
+ version: "2.12.6",
1229
1310
  description: "Model Control Protocol server for TeamCity CI/CD integration with AI coding assistants",
1230
1311
  mcpName: "io.github.Daghis/teamcity",
1231
1312
  main: "dist/index.js",
@@ -4847,6 +4928,7 @@ var esm_default = axiosRetry;
4847
4928
  // src/teamcity/auth.ts
4848
4929
  var import_crypto = require("crypto");
4849
4930
  init_errors();
4931
+ init_stream();
4850
4932
  var asTimingMetaContainer = (value) => {
4851
4933
  if (typeof value === "object" && value !== null) {
4852
4934
  return value;
@@ -4891,10 +4973,46 @@ function logResponse(response) {
4891
4973
  });
4892
4974
  return response;
4893
4975
  }
4894
- function logAndTransformError(error3) {
4976
+ var STREAM_BODY_TIMEOUT_MS = 5e3;
4977
+ var streamToString = async (stream, maxBytes = 64 * 1024) => {
4978
+ const chunks = [];
4979
+ let total = 0;
4980
+ for await (const chunk of stream) {
4981
+ const buf = toBuffer(chunk);
4982
+ chunks.push(buf);
4983
+ total += buf.length;
4984
+ if (total >= maxBytes) {
4985
+ break;
4986
+ }
4987
+ }
4988
+ return Buffer.concat(chunks).toString("utf8").slice(0, maxBytes);
4989
+ };
4990
+ var snapshotStreamBody = async (stream, timeoutMs) => {
4991
+ let timer;
4992
+ const deadline = new Promise((_resolve, reject) => {
4993
+ timer = setTimeout(() => reject(new Error("Timed out reading streamed error body")), timeoutMs);
4994
+ });
4995
+ try {
4996
+ return await Promise.race([streamToString(stream), deadline]);
4997
+ } catch {
4998
+ destroyStream(stream);
4999
+ return void 0;
5000
+ } finally {
5001
+ clearTimeout(timer);
5002
+ }
5003
+ };
5004
+ async function logAndTransformError(error3) {
4895
5005
  if (error3 instanceof TeamCityAPIError) {
4896
5006
  return Promise.reject(error3);
4897
5007
  }
5008
+ const response = error3.response;
5009
+ if (response && isReadableStream(response.data)) {
5010
+ const requestTimeout = error3.config?.timeout ?? 0;
5011
+ response.data = await snapshotStreamBody(
5012
+ response.data,
5013
+ requestTimeout > 0 ? Math.min(requestTimeout, STREAM_BODY_TIMEOUT_MS) : STREAM_BODY_TIMEOUT_MS
5014
+ );
5015
+ }
4898
5016
  const requestId = error3.config?.requestId;
4899
5017
  const tcError = TeamCityAPIError.fromAxiosError(error3, requestId);
4900
5018
  const meta = asTimingMetaContainer(error3.config)?._tcMeta;
@@ -4950,6 +5068,7 @@ function validateConfiguration(baseUrl, token) {
4950
5068
 
4951
5069
  // src/api-client.ts
4952
5070
  init_errors();
5071
+ init_stream();
4953
5072
 
4954
5073
  // src/teamcity-client/api/agent-api.ts
4955
5074
  var import_axios5 = __toESM(require("axios"));
@@ -38427,6 +38546,24 @@ var extractRetryAfterMilliseconds = (value) => {
38427
38546
  }
38428
38547
  return void 0;
38429
38548
  };
38549
+ var toOptionalLineNumber = (value) => {
38550
+ if (value === void 0 || value === null || value === "") {
38551
+ return void 0;
38552
+ }
38553
+ const parsed = Number(value);
38554
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
38555
+ };
38556
+ var sliceLogData = (data, startLine, lineCount) => {
38557
+ if (isReadableStream(data)) {
38558
+ return sliceStreamLines(data, startLine, lineCount);
38559
+ }
38560
+ if (typeof data === "string") {
38561
+ const lines = data.split("\n");
38562
+ const end = lineCount === void 0 ? void 0 : startLine + lineCount;
38563
+ return lines.slice(startLine, end).join("\n");
38564
+ }
38565
+ return data;
38566
+ };
38430
38567
  var TeamCityAPI = class _TeamCityAPI {
38431
38568
  static instance;
38432
38569
  static instanceConfig;
@@ -38491,6 +38628,7 @@ var TeamCityAPI = class _TeamCityAPI {
38491
38628
  this.http = this.axiosInstance;
38492
38629
  esm_default(this.axiosInstance, {
38493
38630
  retries: 3,
38631
+ onRetry: (_retryCount, error3) => discardStreamBody(error3.response?.data),
38494
38632
  retryDelay: (retryCount, error3) => {
38495
38633
  const reqId = error3?.config?.requestId;
38496
38634
  const tcError = TeamCityAPIError.fromAxiosError(error3, reqId);
@@ -38748,27 +38886,47 @@ var TeamCityAPI = class _TeamCityAPI {
38748
38886
  );
38749
38887
  }
38750
38888
  async downloadBuildLog(buildId, options) {
38889
+ const responseType = options?.responseType ?? "text";
38890
+ const rawHeaders = options?.headers ?? void 0;
38891
+ const headers = { Accept: "text/plain", ...rawHeaders ?? {} };
38892
+ const transformResponse = options?.transformResponse ?? [(data) => data];
38751
38893
  const rawParams = options?.params ?? void 0;
38752
- const params = rawParams ? { ...rawParams } : {};
38753
- if (!Object.prototype.hasOwnProperty.call(params, "plain")) {
38754
- params["plain"] = true;
38894
+ const { start, count, ...primaryParams } = rawParams ?? {};
38895
+ const startLine = toOptionalLineNumber(start);
38896
+ const lineCount = toOptionalLineNumber(count);
38897
+ try {
38898
+ const response = await this.axiosInstance.get(`/downloadBuildLog.html`, {
38899
+ ...options,
38900
+ params: { ...primaryParams, buildId },
38901
+ headers,
38902
+ responseType,
38903
+ transformResponse
38904
+ });
38905
+ if (startLine === void 0 && lineCount === void 0) {
38906
+ return response;
38907
+ }
38908
+ return { ...response, data: sliceLogData(response.data, startLine ?? 0, lineCount) };
38909
+ } catch (primaryError) {
38910
+ const status = primaryError instanceof TeamCityAPIError ? primaryError.statusCode : void 0;
38911
+ if (status === void 0 || [401, 403, 429, 503].includes(status)) {
38912
+ throw primaryError;
38913
+ }
38914
+ const params = rawParams ? { ...rawParams } : {};
38915
+ if (!Object.prototype.hasOwnProperty.call(params, "plain")) {
38916
+ params["plain"] = true;
38917
+ }
38918
+ try {
38919
+ return await this.axiosInstance.get(`/app/rest/builds/${toBuildLocator(buildId)}/log`, {
38920
+ ...options,
38921
+ params,
38922
+ headers,
38923
+ responseType,
38924
+ transformResponse
38925
+ });
38926
+ } catch {
38927
+ throw primaryError;
38928
+ }
38755
38929
  }
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
38930
  }
38773
38931
  async getBuildStatistics(buildId, fields) {
38774
38932
  return this.builds.getBuildStatisticValues(toBuildLocator(buildId), fields);
@@ -38857,7 +39015,7 @@ var extraHeadersEqual = (a, b) => {
38857
39015
  };
38858
39016
 
38859
39017
  // src/tools.ts
38860
- var isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
39018
+ var isReadableStream2 = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
38861
39019
  var isAxios4042 = (error3) => (0, import_axios36.isAxiosError)(error3) && error3.response?.status === 404;
38862
39020
  var buildIdentifierSchema = import_zod4.z.object({
38863
39021
  buildId: import_zod4.z.string().min(1).optional(),
@@ -38982,7 +39140,7 @@ var writeArtifactStreamToDisk = async (artifact, stream, options) => {
38982
39140
  var buildArtifactPayload = async (artifact, encoding, options) => {
38983
39141
  if (encoding === "stream") {
38984
39142
  const contentStream = artifact.content;
38985
- if (!isReadableStream(contentStream)) {
39143
+ if (!isReadableStream2(contentStream)) {
38986
39144
  throw new Error("Streaming download did not return a readable stream");
38987
39145
  }
38988
39146
  const { outputPath, bytesWritten } = await writeArtifactStreamToDisk(
@@ -39066,7 +39224,7 @@ var downloadArtifactByUrl = async (adapter, request, encoding, options) => {
39066
39224
  }
39067
39225
  if (encoding === "stream") {
39068
39226
  const stream = response.data;
39069
- if (!isReadableStream(stream)) {
39227
+ if (!isReadableStream2(stream)) {
39070
39228
  throw new Error("Streaming download did not return a readable stream");
39071
39229
  }
39072
39230
  const artifact2 = {
@@ -40237,7 +40395,7 @@ var DEV_TOOLS = [
40237
40395
  responseType: "stream"
40238
40396
  });
40239
40397
  const stream = response.data;
40240
- if (!isReadableStream(stream)) {
40398
+ if (!isReadableStream2(stream)) {
40241
40399
  throw new Error("Streaming log download did not return a readable stream");
40242
40400
  }
40243
40401
  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.6",
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.6",
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.6",
17
17
  "runtimeHint": "npx",
18
18
  "runtimeArguments": [
19
19
  {