@daghis/teamcity-mcp 1.6.0 → 1.7.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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.0](https://github.com/Daghis/teamcity-mcp/compare/v1.6.0...v1.7.0) (2025-09-20)
4
+
5
+
6
+ ### Features
7
+
8
+ * **tools:** add streaming mode to fetch_build_log ([#171](https://github.com/Daghis/teamcity-mcp/issues/171)) ([1abce69](https://github.com/Daghis/teamcity-mcp/commit/1abce69b7fa3866ac289501e2369d5d06d57d57f))
9
+
3
10
  ## [1.6.0](https://github.com/Daghis/teamcity-mcp/compare/v1.5.0...v1.6.0) (2025-09-20)
4
11
 
5
12
 
package/dist/index.js CHANGED
@@ -2117,6 +2117,7 @@ function createAdapterFromTeamCityAPI(api, options = {}) {
2117
2117
  builds: buildApi,
2118
2118
  listBuildArtifacts: (buildId, options2) => api.listBuildArtifacts(buildId, options2),
2119
2119
  downloadArtifactContent: (buildId, artifactPath, requestOptions) => api.downloadBuildArtifact(buildId, artifactPath, requestOptions),
2120
+ downloadBuildLogContent: (buildId, requestOptions) => api.downloadBuildLog(buildId, requestOptions),
2120
2121
  getBuildStatistics: (buildId, fields) => api.getBuildStatistics(buildId, fields),
2121
2122
  listChangesForBuild: (buildId, fields) => api.listChangesForBuild(buildId, fields),
2122
2123
  listSnapshotDependencies: (buildId) => api.listSnapshotDependencies(buildId),
@@ -36696,6 +36697,26 @@ var TeamCityAPI = class _TeamCityAPI {
36696
36697
  requestOptions
36697
36698
  );
36698
36699
  }
36700
+ async downloadBuildLog(buildId, options) {
36701
+ const rawParams = options?.params ?? void 0;
36702
+ const params = rawParams ? { ...rawParams } : {};
36703
+ if (!Object.prototype.hasOwnProperty.call(params, "plain")) {
36704
+ params["plain"] = true;
36705
+ }
36706
+ const rawHeaders = options?.headers ?? void 0;
36707
+ const headers = rawHeaders ? { ...rawHeaders } : {};
36708
+ const requestOptions = {
36709
+ ...options,
36710
+ params,
36711
+ headers: {
36712
+ Accept: "text/plain",
36713
+ ...headers
36714
+ },
36715
+ responseType: options?.responseType ?? "text",
36716
+ transformResponse: options?.transformResponse ?? [(data) => data]
36717
+ };
36718
+ return this.axiosInstance.get(`/app/rest/builds/id:${buildId}/log`, requestOptions);
36719
+ }
36699
36720
  async getBuildStatistics(buildId, fields) {
36700
36721
  return this.builds.getBuildStatisticValues(toBuildLocator(buildId), fields);
36701
36722
  }
@@ -36764,6 +36785,7 @@ var TeamCityAPI = class _TeamCityAPI {
36764
36785
  };
36765
36786
 
36766
36787
  // src/tools.ts
36788
+ var isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
36767
36789
  function getMCPMode2() {
36768
36790
  return getMCPMode();
36769
36791
  }
@@ -37160,7 +37182,17 @@ var DEV_TOOLS = [
37160
37182
  pageSize: { type: "number", description: "Lines per page (default 500)" },
37161
37183
  startLine: { type: "number", description: "0-based start line (overrides page)" },
37162
37184
  lineCount: { type: "number", description: "Max lines to return (overrides pageSize)" },
37163
- tail: { type: "boolean", description: "Tail mode: return last N lines" }
37185
+ tail: { type: "boolean", description: "Tail mode: return last N lines" },
37186
+ encoding: {
37187
+ type: "string",
37188
+ description: "Response encoding: 'text' (default) or 'stream'",
37189
+ enum: ["text", "stream"],
37190
+ default: "text"
37191
+ },
37192
+ outputPath: {
37193
+ type: "string",
37194
+ description: "Optional absolute path to write streamed logs; defaults to a temp file when streaming"
37195
+ }
37164
37196
  },
37165
37197
  required: []
37166
37198
  },
@@ -37173,9 +37205,24 @@ var DEV_TOOLS = [
37173
37205
  pageSize: import_zod4.z.number().int().min(1).max(5e3).optional(),
37174
37206
  startLine: import_zod4.z.number().int().min(0).optional(),
37175
37207
  lineCount: import_zod4.z.number().int().min(1).max(5e3).optional(),
37176
- tail: import_zod4.z.boolean().optional()
37177
- }).refine((v) => Boolean(v.buildId) || Boolean(v.buildNumber), {
37178
- message: "Provide either buildId or buildNumber"
37208
+ tail: import_zod4.z.boolean().optional(),
37209
+ encoding: import_zod4.z.enum(["text", "stream"]).default("text"),
37210
+ outputPath: import_zod4.z.string().min(1).optional()
37211
+ }).superRefine((value, ctx) => {
37212
+ if (!value.buildId && typeof value.buildNumber === "undefined") {
37213
+ ctx.addIssue({
37214
+ code: import_zod4.z.ZodIssueCode.custom,
37215
+ message: "Provide either buildId or buildNumber",
37216
+ path: ["buildId"]
37217
+ });
37218
+ }
37219
+ if (value.encoding === "stream" && value.tail) {
37220
+ ctx.addIssue({
37221
+ code: import_zod4.z.ZodIssueCode.custom,
37222
+ message: "Streaming mode does not support tail queries",
37223
+ path: ["tail"]
37224
+ });
37225
+ }
37179
37226
  });
37180
37227
  return runTool(
37181
37228
  "fetch_build_log",
@@ -37252,10 +37299,22 @@ var DEV_TOOLS = [
37252
37299
  }
37253
37300
  return false;
37254
37301
  };
37302
+ const normalizeError = (error2) => {
37303
+ if ((0, import_axios35.isAxiosError)(error2)) {
37304
+ const status = error2.response?.status;
37305
+ const statusText = (error2.response?.statusText ?? "").trim();
37306
+ const base = status ? `${status}${statusText ? ` ${statusText}` : ""}` : error2.message;
37307
+ return new Error(base || "Request failed");
37308
+ }
37309
+ if (error2 instanceof Error) {
37310
+ return error2;
37311
+ }
37312
+ return new Error(String(error2));
37313
+ };
37255
37314
  const wait = (ms) => new Promise((resolve) => {
37256
37315
  setTimeout(resolve, ms);
37257
37316
  });
37258
- const attemptFetch = async () => {
37317
+ const attemptBuffered = async () => {
37259
37318
  if (typed.tail) {
37260
37319
  const count = typed.lineCount ?? typed.pageSize ?? 500;
37261
37320
  const full = await adapter.getBuildLog(effectiveBuildId);
@@ -37303,16 +37362,52 @@ var DEV_TOOLS = [
37303
37362
  }
37304
37363
  });
37305
37364
  };
37306
- const maxAttempts = typed.tail ? 5 : 3;
37365
+ const attemptStream = async () => {
37366
+ const effectivePageSize = typed.lineCount ?? typed.pageSize ?? 500;
37367
+ const startLine = typeof typed.startLine === "number" ? typed.startLine : ((typed.page ?? 1) - 1) * effectivePageSize;
37368
+ const response = await adapter.downloadBuildLogContent(effectiveBuildId, {
37369
+ params: {
37370
+ start: startLine,
37371
+ count: effectivePageSize
37372
+ },
37373
+ responseType: "stream"
37374
+ });
37375
+ const stream = response.data;
37376
+ if (!isReadableStream(stream)) {
37377
+ throw new Error("Streaming log download did not return a readable stream");
37378
+ }
37379
+ const safeBuildId = effectiveBuildId.replace(/[^a-zA-Z0-9._-]/g, "_") || "build";
37380
+ const defaultFileName = `build-log-${safeBuildId}-${startLine}-${(0, import_node_crypto.randomUUID)()}.log`;
37381
+ const targetPath = typed.outputPath ?? (0, import_node_path.join)((0, import_node_os.tmpdir)(), defaultFileName);
37382
+ await import_node_fs.promises.mkdir((0, import_node_path.dirname)(targetPath), { recursive: true });
37383
+ await (0, import_promises.pipeline)(stream, (0, import_node_fs.createWriteStream)(targetPath));
37384
+ const stats = await import_node_fs.promises.stat(targetPath);
37385
+ const page = Math.floor(startLine / effectivePageSize) + 1;
37386
+ return json({
37387
+ encoding: "stream",
37388
+ outputPath: targetPath,
37389
+ bytesWritten: stats.size,
37390
+ meta: {
37391
+ buildId: effectiveBuildId,
37392
+ buildNumber: typeof typed.buildNumber !== "undefined" ? String(typed.buildNumber) : void 0,
37393
+ buildTypeId: typed.buildTypeId,
37394
+ page,
37395
+ pageSize: effectivePageSize,
37396
+ startLine
37397
+ }
37398
+ });
37399
+ };
37400
+ const isStream = typed.encoding === "stream";
37401
+ const maxAttempts = isStream ? 3 : typed.tail ? 5 : 3;
37307
37402
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
37308
37403
  try {
37309
- return await attemptFetch();
37404
+ return await (isStream ? attemptStream() : attemptBuffered());
37310
37405
  } catch (error2) {
37311
37406
  if (shouldRetry2(error2) && attempt < maxAttempts - 1) {
37312
37407
  await wait(500 * (attempt + 1));
37313
37408
  continue;
37314
37409
  }
37315
- throw error2;
37410
+ throw normalizeError(error2);
37316
37411
  }
37317
37412
  }
37318
37413
  throw new Error("Unable to fetch build log after retries");
@@ -38321,7 +38416,6 @@ var DEV_TOOLS = [
38321
38416
  maxSize: import_zod4.z.number().int().positive().optional(),
38322
38417
  outputPath: import_zod4.z.string().min(1).optional()
38323
38418
  });
38324
- const isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
38325
38419
  const toTempFilePath = (artifactName) => {
38326
38420
  const base = (0, import_node_path.basename)(artifactName || "artifact");
38327
38421
  const safeStem = base.replace(/[^a-zA-Z0-9._-]/g, "_") || "artifact";