Skip to content

Commit cf60864

Browse files
feat(mcp): Add pagination parameters to get_cloud_sync_logs tool (#888)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 5b9f2fa commit cf60864

File tree

1 file changed

+105
-8
lines changed

1 file changed

+105
-8
lines changed

airbyte/mcp/cloud_ops.py

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,23 @@ class CloudWorkspaceResult(BaseModel):
160160
"""ID of the organization this workspace belongs to."""
161161

162162

163+
class LogReadResult(BaseModel):
164+
"""Result of reading sync logs with pagination support."""
165+
166+
job_id: int
167+
"""The job ID the logs belong to."""
168+
attempt_number: int
169+
"""The attempt number the logs belong to."""
170+
log_text: str
171+
"""The string containing the log text we are returning."""
172+
log_text_start_line: int
173+
"""1-based line index of the first line returned."""
174+
log_text_line_count: int
175+
"""Count of lines we are returning."""
176+
total_log_lines_available: int
177+
"""Total number of log lines available, shows if any lines were missed due to the limit."""
178+
179+
163180
def _get_cloud_workspace(workspace_id: str | None = None) -> CloudWorkspace:
164181
"""Get an authenticated CloudWorkspace.
165182
@@ -802,20 +819,67 @@ def get_cloud_sync_logs(
802819
default=None,
803820
),
804821
],
805-
) -> str:
822+
max_lines: Annotated[
823+
int,
824+
Field(
825+
description=(
826+
"Maximum number of lines to return. "
827+
"Defaults to 4000 if not specified. "
828+
"If '0' is provided, no limit is applied."
829+
),
830+
default=4000,
831+
),
832+
],
833+
from_tail: Annotated[
834+
bool | None,
835+
Field(
836+
description=(
837+
"Pull from the end of the log text if total lines is greater than 'max_lines'. "
838+
"Defaults to True if `line_offset` is not specified. "
839+
"Cannot combine `from_tail=True` with `line_offset`."
840+
),
841+
default=None,
842+
),
843+
],
844+
line_offset: Annotated[
845+
int | None,
846+
Field(
847+
description=(
848+
"Number of lines to skip from the beginning of the logs. "
849+
"Cannot be combined with `from_tail=True`."
850+
),
851+
default=None,
852+
),
853+
],
854+
) -> LogReadResult:
806855
"""Get the logs from a sync job attempt on Airbyte Cloud."""
856+
# Validate that line_offset and from_tail are not both set
857+
if line_offset is not None and from_tail:
858+
raise PyAirbyteInputError(
859+
message="Cannot specify both 'line_offset' and 'from_tail' parameters.",
860+
context={"line_offset": line_offset, "from_tail": from_tail},
861+
)
862+
863+
if from_tail is None and line_offset is None:
864+
from_tail = True
807865
workspace: CloudWorkspace = _get_cloud_workspace(workspace_id)
808866
connection = workspace.get_connection(connection_id=connection_id)
809867

810868
sync_result: cloud.SyncResult | None = connection.get_sync_result(job_id=job_id)
811869

812870
if not sync_result:
813-
return f"No sync job found for connection '{connection_id}'"
871+
raise AirbyteMissingResourceError(
872+
resource_type="sync job",
873+
resource_name_or_id=connection_id,
874+
)
814875

815876
attempts = sync_result.get_attempts()
816877

817878
if not attempts:
818-
return f"No attempts found for job '{sync_result.job_id}'"
879+
raise AirbyteMissingResourceError(
880+
resource_type="sync attempt",
881+
resource_name_or_id=str(sync_result.job_id),
882+
)
819883

820884
if attempt_number is not None:
821885
target_attempt = None
@@ -825,19 +889,52 @@ def get_cloud_sync_logs(
825889
break
826890

827891
if target_attempt is None:
828-
return f"Attempt number {attempt_number} not found for job '{sync_result.job_id}'"
892+
raise AirbyteMissingResourceError(
893+
resource_type="sync attempt",
894+
resource_name_or_id=f"job {sync_result.job_id}, attempt {attempt_number}",
895+
)
829896
else:
830897
target_attempt = max(attempts, key=lambda a: a.attempt_number)
831898

832899
logs = target_attempt.get_full_log_text()
833900

834901
if not logs:
835-
return (
836-
f"No logs available for job '{sync_result.job_id}', "
837-
f"attempt {target_attempt.attempt_number}"
902+
# Return empty result with zero lines
903+
return LogReadResult(
904+
log_text=(
905+
f"[No logs available for job '{sync_result.job_id}', "
906+
f"attempt {target_attempt.attempt_number}.]"
907+
),
908+
log_text_start_line=1,
909+
log_text_line_count=0,
910+
total_log_lines_available=0,
911+
job_id=sync_result.job_id,
912+
attempt_number=target_attempt.attempt_number,
838913
)
839914

840-
return logs
915+
# Apply line limiting
916+
log_lines = logs.splitlines()
917+
total_lines = len(log_lines)
918+
919+
# Determine effective max_lines (0 means no limit)
920+
effective_max = total_lines if max_lines == 0 else max_lines
921+
922+
# Calculate start_index and slice based on from_tail or line_offset
923+
if from_tail:
924+
start_index = max(0, total_lines - effective_max)
925+
selected_lines = log_lines[start_index:][:effective_max]
926+
else:
927+
start_index = line_offset or 0
928+
selected_lines = log_lines[start_index : start_index + effective_max]
929+
930+
return LogReadResult(
931+
log_text="\n".join(selected_lines),
932+
log_text_start_line=start_index + 1, # Convert to 1-based index
933+
log_text_line_count=len(selected_lines),
934+
total_log_lines_available=total_lines,
935+
job_id=sync_result.job_id,
936+
attempt_number=target_attempt.attempt_number,
937+
)
841938

842939

843940
@mcp_tool(

0 commit comments

Comments
 (0)