feat: add pagination support for CI job logs to prevent context window flooding

- Add optional `limit` and `offset` parameters to get_pipeline_job_output
- Default limit is 1000 lines when pagination is used
- Returns lines from the end of the log, with configurable offset
- Includes truncation metadata showing what was skipped
- Maintains backward compatibility (no parameters = full log)

This prevents large CI logs from overwhelming AI tooling context windows
while still providing access to the most recent and relevant log output.
This commit is contained in:
Simon Huerlimann
2025-06-11 10:48:14 +02:00
parent 1ba54342bc
commit 2905f30af7
2 changed files with 37 additions and 5 deletions

View File

@ -518,7 +518,7 @@ const allTools = [
}, },
{ {
name: "get_pipeline_job_output", name: "get_pipeline_job_output",
description: "Get the output/trace of a GitLab pipeline job number", description: "Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage",
inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
}, },
{ {
@ -2606,9 +2606,11 @@ async function getPipelineJob(projectId: string, jobId: number): Promise<GitLabP
* *
* @param {string} projectId - The ID or URL-encoded path of the project * @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} jobId - The ID of the job * @param {number} jobId - The ID of the job
* @param {number} limit - Maximum number of lines to return from the end (default: 1000)
* @param {number} offset - Number of lines to skip from the end (default: 0)
* @returns {Promise<string>} The job output/trace * @returns {Promise<string>} The job output/trace
*/ */
async function getPipelineJobOutput(projectId: string, jobId: number): Promise<string> { async function getPipelineJobOutput(projectId: string, jobId: number, limit?: number, offset?: number): Promise<string> {
projectId = decodeURIComponent(projectId); // Decode project ID projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL( const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/trace` `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/trace`
@ -2627,7 +2629,35 @@ async function getPipelineJobOutput(projectId: string, jobId: number): Promise<s
} }
await handleGitLabError(response); await handleGitLabError(response);
return await response.text(); const fullTrace = await response.text();
// Apply client-side pagination to limit context window usage
if (limit !== undefined || offset !== undefined) {
const lines = fullTrace.split('\n');
const startOffset = offset || 0;
const maxLines = limit || 1000;
// Return lines from the end, skipping offset lines and limiting to maxLines
const startIndex = Math.max(0, lines.length - startOffset - maxLines);
const endIndex = lines.length - startOffset;
const selectedLines = lines.slice(startIndex, endIndex);
const result = selectedLines.join('\n');
// Add metadata about truncation
if (startIndex > 0 || endIndex < lines.length) {
const totalLines = lines.length;
const shownLines = selectedLines.length;
const skippedFromStart = startIndex;
const skippedFromEnd = startOffset;
return `[Log truncated: showing ${shownLines} of ${totalLines} lines, skipped ${skippedFromStart} from start, ${skippedFromEnd} from end]\n\n${result}`;
}
return result;
}
return fullTrace;
} }
/** /**
@ -3722,8 +3752,8 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
} }
case "get_pipeline_job_output": { case "get_pipeline_job_output": {
const { project_id, job_id } = GetPipelineJobOutputSchema.parse(request.params.arguments); const { project_id, job_id, limit, offset } = GetPipelineJobOutputSchema.parse(request.params.arguments);
const jobOutput = await getPipelineJobOutput(project_id, job_id); const jobOutput = await getPipelineJobOutput(project_id, job_id, limit, offset);
return { return {
content: [ content: [
{ {

View File

@ -191,6 +191,8 @@ export const CancelPipelineSchema = z.object({
export const GetPipelineJobOutputSchema = z.object({ export const GetPipelineJobOutputSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), project_id: z.string().describe("Project ID or URL-encoded path"),
job_id: z.number().describe("The ID of the job"), job_id: z.number().describe("The ID of the job"),
limit: z.number().optional().describe("Maximum number of lines to return from the end of the log (default: 1000)"),
offset: z.number().optional().describe("Number of lines to skip from the end of the log (default: 0)"),
}); });
// User schemas // User schemas