Grep an Octopus task activity log
grep_task_logSearch Octopus Deploy task activity logs with grep-style patterns to find specific errors, steps, or text. Returns only matching lines with optional context, avoiding full-log downloads.
Instructions
Search a server task's activity log with grep-style semantics. Returns only matching lines (with optional symmetric context windows). This is the canonical way to inspect task logs — there is no full-log resource URI, because exposing one would tempt callers to inhale multi-megabyte bodies when grep is almost always the better primitive.
Use this when you know what to look for (a specific error string, a step name, a pattern). For structured access to the activity tree (step hierarchy, categories, timing) use the octopus://spaces/{spaceName}/tasks/{taskId}/details resource instead.
Parameter conventions mirror GNU grep so the schema is self-explanatory:
pattern (regex by default; set fixedString:true for literal text)
caseInsensitive (-i)
invertMatch (-v)
fixedString (-F)
beforeContext (-B)
afterContext (-A)
maxCount (-m)
Response includes totalMatches (true count across the whole log), totalLines, the matched lines with 1-indexed lineNumber, optional before/after context arrays, and a taskDetailsResourceUri for the structured fall-through.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| spaceName | Yes | Octopus space name. Case-sensitive. | |
| taskId | Yes | ServerTasks-XXXX ID. Use find_releases or list_deployments to discover task IDs from their parent entities. | |
| pattern | Yes | Regex (default) or literal substring (when fixedString=true). Anchors and groups behave as in JavaScript RegExp. Tested against each log line independently — the same model as `grep`. | |
| caseInsensitive | No | Equivalent to grep -i. Default false. | |
| invertMatch | No | Equivalent to grep -v: return lines that do NOT match. Default false. | |
| fixedString | No | Equivalent to grep -F: treat pattern as a literal substring, not a regex. Use this when grepping for text containing regex metacharacters. Default false. | |
| beforeContext | No | Equivalent to grep -B: lines of preceding context to include with each match. Capped at 50. | |
| afterContext | No | Equivalent to grep -A: lines of trailing context to include with each match. Capped at 50. | |
| maxCount | No | Equivalent to grep -m: stop returning matches after this many. totalMatches in the response still reflects the true count across the whole log. Hard cap 500. | |
| stripPrefixes | No | Strip the timestamp/level prefix (e.g. `04:36:40 Fatal | `) from each line before pattern matching AND in the returned line/context text. Default false. Turn this on when greping for words that collide with level names (Fatal, Error, Warn) or when you want clean message-only output. Note: when on, your pattern will not match against the prefix — searching for `Fatal` won't find Fatal-level lines. |
Implementation Reference
- src/tools/grepTaskLog.ts:102-178 (handler)The full tool implementation: registerGrepTaskLogTool function registers 'grep_task_log' on the MCP server. The handler validates the task ID, creates an API client, fetches the raw activity log via SpaceServerTaskRepository.getRaw(), passes it through the grepLines() helper, and returns a JSON result with matches, context, and a taskDetailsResourceUri for structured fall-through.
export function registerGrepTaskLogTool(server: McpServer) { server.registerTool( "grep_task_log", { title: "Grep an Octopus task activity log", description: `Search a server task's activity log with grep-style semantics. Returns only matching lines (with optional symmetric context windows). This is the canonical way to inspect task logs — there is no full-log resource URI, because exposing one would tempt callers to inhale multi-megabyte bodies when grep is almost always the better primitive. Use this when you know what to look for (a specific error string, a step name, a pattern). For structured access to the activity tree (step hierarchy, categories, timing) use the octopus://spaces/{spaceName}/tasks/{taskId}/details resource instead. Parameter conventions mirror GNU grep so the schema is self-explanatory: - pattern (regex by default; set fixedString:true for literal text) - caseInsensitive (-i) - invertMatch (-v) - fixedString (-F) - beforeContext (-B) - afterContext (-A) - maxCount (-m) Response includes totalMatches (true count across the whole log), totalLines, the matched lines with 1-indexed lineNumber, optional before/after context arrays, and a taskDetailsResourceUri for the structured fall-through.`, inputSchema, annotations: READ_ONLY_TOOL_ANNOTATIONS, }, async (args) => { const params = args as GrepTaskLogParams; const { spaceName, taskId } = params; validateEntityId(taskId, "task", ENTITY_PREFIXES.task); try { const client = await Client.create( getClientConfigurationFromEnvironment(), ); const rawLog = await new SpaceServerTaskRepository( client, spaceName, ).getRaw(taskId); const { totalLines, totalMatches, matches } = grepLines(rawLog, params); const result: GrepTaskLogResult = { spaceName, taskId, pattern: params.pattern, totalLines, totalMatches, returnedMatches: matches.length, truncated: totalMatches > matches.length, matches, taskDetailsResourceUri: `octopus://spaces/${encodeURIComponent(spaceName)}/tasks/${encodeURIComponent(taskId)}/details`, }; return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; } catch (error) { handleOctopusApiError(error, { entityType: "task", entityId: taskId, spaceName, helpText: "Use find_releases or list_deployments to discover task IDs via their parent entity. Use get_task_from_url to resolve a task ID from an Octopus portal URL.", }); } }, ); } registerToolDefinition({ toolName: "grep_task_log", config: { toolset: "tasks", readOnly: true }, registerFn: registerGrepTaskLogTool, }); - src/tools/grepTaskLog.ts:19-100 (schema)GrepTaskLogParams and GrepTaskLogResult TypeScript interfaces (lines 19-46) plus the inputSchema zod validation object (lines 48-100). Defines all grep-style parameters: spaceName, taskId, pattern, caseInsensitive, invertMatch, fixedString, beforeContext (capped at 50), afterContext (capped at 50), maxCount (capped at 500, default 100), and stripPrefixes.
export interface GrepTaskLogParams { spaceName: string; taskId: string; pattern: string; caseInsensitive?: boolean; invertMatch?: boolean; fixedString?: boolean; beforeContext?: number; afterContext?: number; maxCount?: number; stripPrefixes?: boolean; } export interface GrepTaskLogResult { spaceName: string; taskId: string; pattern: string; totalLines: number; totalMatches: number; returnedMatches: number; truncated: boolean; matches: GrepMatch[]; /** * URI for the structured ActivityLogs tree if the agent needs more than * grep can express (e.g. step hierarchy, category filtering, timing). */ taskDetailsResourceUri: string; } const inputSchema = { spaceName: z .string() .describe("Octopus space name. Case-sensitive."), taskId: z .string() .describe("ServerTasks-XXXX ID. Use find_releases or list_deployments to discover task IDs from their parent entities."), pattern: z .string() .min(1) .describe( "Regex (default) or literal substring (when fixedString=true). Anchors and groups behave as in JavaScript RegExp. Tested against each log line independently — the same model as `grep`.", ), caseInsensitive: z .boolean() .default(false) .describe("Equivalent to grep -i. Default false."), invertMatch: z .boolean() .default(false) .describe("Equivalent to grep -v: return lines that do NOT match. Default false."), fixedString: z .boolean() .default(false) .describe("Equivalent to grep -F: treat pattern as a literal substring, not a regex. Use this when grepping for text containing regex metacharacters. Default false."), beforeContext: z .number() .int() .min(0) .max(MAX_CONTEXT) .default(0) .describe(`Equivalent to grep -B: lines of preceding context to include with each match. Capped at ${MAX_CONTEXT}.`), afterContext: z .number() .int() .min(0) .max(MAX_CONTEXT) .default(0) .describe(`Equivalent to grep -A: lines of trailing context to include with each match. Capped at ${MAX_CONTEXT}.`), maxCount: z .number() .int() .min(1) .max(MAX_COUNT_HARD_CAP) .default(100) .describe(`Equivalent to grep -m: stop returning matches after this many. totalMatches in the response still reflects the true count across the whole log. Hard cap ${MAX_COUNT_HARD_CAP}.`), stripPrefixes: z .boolean() .default(false) .describe( "Strip the timestamp/level prefix (e.g. `04:36:40 Fatal | `) from each line before pattern matching AND in the returned line/context text. Default false. Turn this on when greping for words that collide with level names (Fatal, Error, Warn) or when you want clean message-only output. Note: when on, your pattern will not match against the prefix — searching for `Fatal` won't find Fatal-level lines.", ), }; - src/tools/grepTaskLog.ts:174-178 (registration)Self-registration via registerToolDefinition: marks the tool as part of the 'tasks' toolset, read-only. Also imported in src/tools/index.ts (line 43) to trigger the self-registration side-effect.
registerToolDefinition({ toolName: "grep_task_log", config: { toolset: "tasks", readOnly: true }, registerFn: registerGrepTaskLogTool, }); - src/helpers/grepLines.ts:73-137 (helper)The grepLines() pure function shared by grep_task_log and grep_llms_txt. Splits the raw text into lines, optionally strips log prefixes, compiles the pattern as regex (or literal with fixedString), iterates lines collecting matches with symmetric before/after context windows, and returns totalLines/totalMatches/match array. Implements the hard caps (MAX_CONTEXT=50, MAX_COUNT_HARD_CAP=500).
export function grepLines( rawText: string, params: GrepLinesParams, ): GrepLinesResult { const { pattern, caseInsensitive = false, invertMatch = false, fixedString = false, beforeContext = 0, afterContext = 0, maxCount = 100, stripPrefixes = false, } = params; const rawLines = rawText.split("\n"); // Drop the trailing empty element produced by a final newline. if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") rawLines.pop(); // When stripPrefixes is on, the timestamp/level prefix is removed before // pattern matching AND before output, so the caller's regex doesn't see the // prefix (no false matches against level names) and the returned line/context // text is clean for downstream consumption. const lines = stripPrefixes ? rawLines.map((line) => line.replace(LOG_PREFIX_REGEX, "")) : rawLines; const regex = compilePattern(pattern, caseInsensitive, fixedString); const matches: GrepMatch[] = []; let totalMatches = 0; for (let i = 0; i < lines.length; i++) { const isMatch = regex.test(lines[i]) !== invertMatch; if (!isMatch) continue; totalMatches++; if (matches.length >= maxCount) continue; const match: GrepMatch = { lineNumber: i + 1, line: lines[i], }; if (beforeContext > 0) { const start = Math.max(0, i - beforeContext); match.before = lines.slice(start, i).map((line, idx) => ({ lineNumber: start + idx + 1, line, })); } if (afterContext > 0) { const end = Math.min(lines.length, i + 1 + afterContext); match.after = lines.slice(i + 1, end).map((line, idx) => ({ lineNumber: i + 2 + idx, line, })); } matches.push(match); } return { totalLines: lines.length, totalMatches, matches }; } - src/helpers/grepLines.ts:1-72 (helper)Supporting types (GrepLinesParams, ContextLine, GrepMatch, GrepLinesResult), constants (MAX_CONTEXT=50, MAX_COUNT_HARD_CAP=500), the LOG_PREFIX_REGEX for stripping timestamp/level prefixes, escapeRegExp() for literal mode, and compilePattern() which builds the RegExp.
/** * GNU-grep-shaped line search over a multi-line string. Pure function, used by * grep_task_log (task activity logs) and grep_llms_txt (catalog markdown). Each * line is tested independently; matching lines are emitted with optional * symmetric context windows. Overlapping context between adjacent matches is * NOT deduplicated — each match carries its own complete context window so the * consumer can reason about each match in isolation. This is a deliberate * departure from GNU grep's `--`-separated output but it is the right shape * for a JSON tool response. */ export interface GrepLinesParams { pattern: string; caseInsensitive?: boolean; invertMatch?: boolean; fixedString?: boolean; beforeContext?: number; afterContext?: number; maxCount?: number; stripPrefixes?: boolean; } export interface ContextLine { lineNumber: number; line: string; } export interface GrepMatch { lineNumber: number; line: string; before?: ContextLine[]; after?: ContextLine[]; } export interface GrepLinesResult { totalLines: number; totalMatches: number; matches: GrepMatch[]; } export const MAX_CONTEXT = 50; export const MAX_COUNT_HARD_CAP = 500; // Octopus task-log line prefix: an optional ISO date, a HH:MM:SS time, one or // more spaces, a level token (Info/Warn/Error/Fatal/Verbose/etc.), one or more // spaces, a pipe, and an optional space. Examples: // "04:36:40 Fatal | message" (real server output) // "2026-05-05T12:00:00 Info | message" (test fixtures) // Lines that don't match the shape are left untouched. const LOG_PREFIX_REGEX = /^(?:\d{4}-\d{2}-\d{2}T)?\d{2}:\d{2}:\d{2}\s+\S+\s+\|\s?/; function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function compilePattern( pattern: string, caseInsensitive: boolean, fixedString: boolean, ): RegExp { const source = fixedString ? escapeRegExp(pattern) : pattern; const flags = caseInsensitive ? "i" : ""; try { return new RegExp(source, flags); } catch (error) { throw new Error( `Invalid pattern: ${error instanceof Error ? error.message : String(error)}. ` + "Set fixedString:true to treat the pattern as a literal substring instead of a regex.", ); } }