Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.80.0",
"version": "0.81.0",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,9 @@ kbagent context
kbagent init [--from-global] [--project ALIAS ...]
# `--project ALIAS` (repeatable) copies only the named project(s) from the global config and implies --from-global.
kbagent doctor [--fix]
# `doctor` includes an `mcp_tool_tasks` check (0.81.0+): warns about scheduled agent tasks
# still using `--type mcp_tool` (removed in v0.85.0); they run unattended and get no warning
# at removal. `agent list` marks those rows and adds a per-task `deprecation` key in --json.
kbagent version [--beta]
kbagent update [--beta]
# `--beta` (or env `KBAGENT_INCLUDE_PRERELEASE=1`) opts into pre-release versions
Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.80.0",
"version": "0.81.0",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ CLI parity for the `/agents` REST surface. Reads/writes `<config_dir>/agents.jso

## Utility
- `init [--from-global] [--project ALIAS ...]` -- create local `.kbagent/` workspace (per-directory isolation); `--project ALIAS` (repeatable) copies only the named project(s) and implies `--from-global`
- `doctor [--fix]` -- health checks; `--fix` auto-installs MCP server binary. Includes a `sync_secrets` check (since 0.55.0): when run inside a sync working tree (`.keboola/manifest.json`), warns if any in-sync config holds plaintext `#`-secrets (#378); `skip` outside a sync tree
- `doctor [--fix]` -- health checks; `--fix` auto-installs MCP server binary. Includes a `sync_secrets` check (since 0.55.0): when run inside a sync working tree (`.keboola/manifest.json`), warns if any in-sync config holds plaintext `#`-secrets (#378); `skip` outside a sync tree. Also an `mcp_tool_tasks` check (since 0.81.0): warns when `agents.json` holds scheduled tasks using the deprecated `mcp_tool` action (removed in v0.85.0), listing each task's tool and native replacement in `details.tasks[]`
- `version` -- show version and check for MCP server updates; on a standalone binary it advertises the native channel's upgrade command instead of `kbagent update` (v0.79.0+)
- `context` -- full usage instructions for AI agents

Expand Down
6 changes: 6 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ Versioning convention:
unlike an interactive `tool call` they get no warning at removal time: a
scheduled task simply starts failing on its next cron tick. Use the
`cli_equivalent` from `kbagent tool list` to rewrite each one.
- **Find the affected tasks (since v0.81.0)**: `kbagent doctor` has an
`mcp_tool_tasks` check that warns with the task ids, the tool each one
calls and its native replacement (`details.tasks[]` in `--json`, each with
`native_command`). `kbagent agent list` marks those rows DEPRECATED and its
`--json` adds an additive per-task `deprecation` key -- present ONLY on
affected tasks, so every other consumer sees a byte-identical payload.

## MCP tool classification is FAIL-CLOSED; parity commands replace `tool call` (since v0.73.0)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-cli"
version = "0.80.0"
version = "0.81.0"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
19 changes: 19 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.81.0": [
"New (#390): `kbagent doctor` gains an `mcp_tool_tasks` check that finds scheduled "
"agent tasks still using the deprecated `--type mcp_tool` action, which is removed "
"in v0.85.0. These are the tasks that break silently: an interactive `tool call` "
"warns on every invocation right up to removal, but a scheduled task was warned "
"once -- when it was created -- and then runs unattended, so at removal it simply "
"starts failing on its next cron tick with nobody watching. The check warns with "
"each task's id, the tool it calls and the native command that replaces it "
"(`details.tasks[]` in `--json`, each carrying `native_command`); a tool the parity "
"map does not know reports `null` rather than inventing a replacement. It skips "
"cleanly when there is no `agents.json` and never spawns MCP or calls the API.",
"New (#390): `kbagent agent list` marks tasks using the MCP passthrough. A note under "
"the table names them and points at `kbagent doctor` (an inline tag in the Type "
"column was truncated to 'DEPRECA…' -- that column is one of seven), and `--json` "
"adds an additive per-task "
"`deprecation` key carrying the removal version. The key appears ONLY on affected "
"tasks, so every existing consumer sees a byte-identical payload -- the same "
"contract `tool list` / `tool call` already use.",
],
"0.80.0": [
"Note (#390): the MCP passthrough now has a named removal date -- `kbagent tool "
"list` / `tool call` and `agent --type mcp_tool` are REMOVED in **v0.85.0**, "
Expand Down
33 changes: 31 additions & 2 deletions src/keboola_agent_cli/commands/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,19 @@ def _resolve_id(
# ── Output renderers ──────────────────────────────────────────────────


def _annotate_deprecation(task: dict[str, Any]) -> dict[str, Any]:
"""Add an additive ``deprecation`` key to a task using the MCP passthrough.

Additive and only on affected tasks, so every existing ``--json`` consumer
sees a byte-identical payload -- the same contract ``tool list`` / ``tool
call`` already use. Machine consumers can therefore find the tasks that
need migrating without parsing the human table.
"""
if (task.get("action") or {}).get("type") == "mcp_tool":
task["deprecation"] = MCP_TOOL_ACTION_DEPRECATION
return task


def _render_tasks_table(console: Any, data: dict[str, Any]) -> None:
"""Plain-text table of tasks: id / name / cron / state."""
tasks = data.get("tasks") or []
Expand All @@ -262,17 +275,33 @@ def _render_tasks_table(console: Any, data: dict[str, Any]) -> None:
state_bits.append("[green]enabled[/green]")
state = " ".join(state_bits) or "-"
action = task.get("action") or {}
action_type = action.get("type", "?")
table.add_row(
str(task.get("id", "")),
task.get("name", ""),
"" if task.get("manual") else task.get("cron", "") or "-",
action.get("type", "?"),
action_type,
state,
task.get("last_run_at") or "-",
task.get("next_run_at") or "-",
)
console.print(table)

# Footnote rather than a marker inside the Type cell: that column is narrow
# enough that Rich truncated the tag to "DEPRECA…", which is worse than not
# flagging it at all. Below the table there is room to name the affected
# tasks AND what to do about them.
deprecated = [t for t in tasks if (t.get("action") or {}).get("type") == "mcp_tool"]
if deprecated:
ids = ", ".join(str(t.get("id", "?")) for t in deprecated)
console.print(
f"[yellow]{len(deprecated)} task(s) use the deprecated 'mcp_tool' action "
f"({ids}) -- REMOVED in kbagent v{MCP_REMOVAL_VERSION} "
f"({MCP_REMOVAL_TARGET_DATE}).[/yellow] They run unattended, so they get no "
f"warning at removal. Run [cyan]kbagent doctor[/cyan] for the native "
f"replacement of each."
)


def _render_task_detail(console: Any, task: dict[str, Any]) -> None:
"""Pretty single-task panel with the action payload pretty-printed."""
Expand Down Expand Up @@ -513,7 +542,7 @@ def agent_list(ctx: typer.Context) -> None:
except ConfigError as exc:
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
raise typer.Exit(code=5) from None
payload = {"tasks": [t.model_dump(mode="json") for t in tasks]}
payload = {"tasks": [_annotate_deprecation(t.model_dump(mode="json")) for t in tasks]}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Serve REST /agents list does not carry the new deprecation key

The additive deprecation key is applied only in the CLI command layer, so kbagent serve's /agents listing and the Web UI keep the old payload and give no hint about tasks that will break at v0.85.0. That matches the PR description (CLI-only, phase 2 part 1), but it means UI users of scheduled tasks — arguably the population least likely to run kbagent doctor — still get no signal. Worth confirming it is planned for part 2.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

formatter.output(payload, _render_tasks_table)


Expand Down
2 changes: 2 additions & 0 deletions src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,8 @@

The `tool` group and `agent --type mcp_tool` are REMOVED in kbagent
v0.85.0, scheduled for the end of August 2026. Migrate before then.
Run `kbagent doctor` to list scheduled tasks still on `mcp_tool` --
the mcp_tool_tasks check names each task's tool and native replacement.
The MCP passthrough is on a removal track (epic #390): every catalog tool
has a native command. Prefer the native command in ALL new work -- `tool
list` prints the replacement in the cli_equivalent column and `tool call`
Expand Down
98 changes: 98 additions & 0 deletions src/keboola_agent_cli/services/doctor_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,28 @@
from ..config_store import ConfigStore
from ..constants import ENV_CONVERSATION_ID
from ..errors import KeboolaApiError
from ..mcp_parity import (
MCP_REMOVAL_TARGET_DATE,
MCP_REMOVAL_VERSION,
native_equivalent,
)
from ..models import AppConfig
from .base import ClientFactory, default_client_factory
from .mcp_service import McpService, ensure_mcp_installed

# Cap on how many offending items a single check names inline; the rest are
# summarised as "+N more" and the full list travels in `details` for --json.
_MAX_LISTED_TASKS = 5
AGENTS_FILENAME = "agents.json"


def _native_command_for(tool: str | None) -> str | None:
"""Native CLI replacement for an MCP tool name, if the parity map knows one."""
if not tool:
return None
entry = native_equivalent(tool)
return f"kbagent {entry.command}" if entry is not None else None


class DoctorService:
"""Business logic for health checks.
Expand Down Expand Up @@ -87,6 +105,9 @@ def run_checks(self) -> dict[str, Any]:
sync_secret_check = self._check_sync_secrets()
all_checks.append(sync_secret_check)

mcp_tool_task_check = self._check_mcp_tool_tasks()
all_checks.append(mcp_tool_task_check)

# Build summary
total = len(all_checks)
passed = sum(1 for c in all_checks if c["status"] == "pass")
Expand Down Expand Up @@ -158,6 +179,83 @@ def _check_sync_secrets(self) -> dict[str, Any]:
),
}

def _check_mcp_tool_tasks(self) -> dict[str, Any]:
"""Check 9: flag scheduled tasks that use the MCP passthrough (epic #390).

``agent --type mcp_tool`` is removed in the version named by
:data:`MCP_REMOVAL_VERSION`. Unlike an interactive ``tool call`` -- which
warns on every invocation right up to removal -- these tasks live in
``agents.json`` and then run unattended, so nobody is present to be
warned when they break: a cron task simply starts failing. This check is
the standing reminder in the one command people run when something feels
off. Read-only: filesystem only, no API call, no MCP spawn.
"""
# Local import: keeps the server package off the doctor cold-start path.
from ..server.agents_store import AgentStore

agents_path = self._config_store.config_dir / AGENTS_FILENAME
if not agents_path.exists():
return {
"check": "mcp_tool_tasks",
"name": "Deprecated mcp_tool agent tasks",
"status": "skip",
"message": f"No {AGENTS_FILENAME} in the config dir -- no agent tasks registered.",
}

try:
tasks = AgentStore(config_dir=self._config_store.config_dir).load_tasks()
except Exception as exc:
return {
"check": "mcp_tool_tasks",
"name": "Deprecated mcp_tool agent tasks",
"status": "warn",
"message": f"Could not read {AGENTS_FILENAME}: {exc}",
}

affected = [t for t in tasks if getattr(t.action, "type", None) == "mcp_tool"]
if not affected:
return {
"check": "mcp_tool_tasks",
"name": "Deprecated mcp_tool agent tasks",
"status": "pass",
"message": f"No tasks use the deprecated 'mcp_tool' action ({len(tasks)} checked).",
}
Comment on lines +205 to +222

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Health check reports a clean result when the scheduled-task file is damaged

A damaged list of scheduled tasks is reported as healthy (the pass branch at src/keboola_agent_cli/services/doctor_service.py:216-222) instead of warning, because the reader silently returns an empty list for an unparsable file, so users are told nothing needs migrating when the check could not actually see their tasks.
Impact: Someone with soon-to-break scheduled tasks and a corrupted task file gets a false all-clear from the one command they run to check for problems.

Why the warn branch is unreachable and what the user sees

AgentStore.load_tasks() (src/keboola_agent_cli/server/agents_store.py:120-136) catches OSError / json.JSONDecodeError itself, logs a warning and returns []. It also skips individual entries that fail validation. Therefore the try/except Exception around the call in _check_mcp_tool_tasks never fires for the realistic failure modes, and the check falls through to the pass result with the message "No tasks use the deprecated 'mcp_tool' action (0 checked)". The PR's own test (tests/test_mcp_tool_task_detection.py:79-85) documents this by accepting either pass or warn. A more accurate behaviour would be to parse agents.json directly (or have the check detect that the file exists but yielded zero tasks) and report warn with the parse error.

Prompt for agents
In DoctorService._check_mcp_tool_tasks (src/keboola_agent_cli/services/doctor_service.py) the try/except around AgentStore.load_tasks() is effectively dead: AgentStore.load_tasks (src/keboola_agent_cli/server/agents_store.py) already swallows OSError and json.JSONDecodeError, logs, and returns an empty list, and also skips entries that fail pydantic validation. As a result a corrupt or unreadable agents.json produces a 'pass' result claiming no tasks use the deprecated mcp_tool action, which is a false all-clear in the exact command users run when they suspect something is wrong. Consider reading/parsing agents.json in the check itself (or detecting the 'file exists but zero tasks parsed' case) so a damaged file yields the intended 'warn' status with the parse error, while a genuinely empty task list still passes.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


shown = ", ".join(
f"{t.id} ({t.action.params.get('tool', '?')})" for t in affected[:_MAX_LISTED_TASKS]
)
more = (
f" (+{len(affected) - _MAX_LISTED_TASKS} more)"
if len(affected) > _MAX_LISTED_TASKS
else ""
)
return {
"check": "mcp_tool_tasks",
"name": "Deprecated mcp_tool agent tasks",
"status": "warn",
"message": (
f"{len(affected)} scheduled task(s) use the deprecated 'mcp_tool' action, "
f"REMOVED in kbagent v{MCP_REMOVAL_VERSION} ({MCP_REMOVAL_TARGET_DATE}): "
f"{shown}{more}. They run unattended, so they get NO warning at removal -- "
f"they just start failing. Migrate each to --type cli_command; "
f"`kbagent tool list` prints the native command per tool."
),
"details": {
"removal_version": MCP_REMOVAL_VERSION,
"tasks": [
{
"id": t.id,
"name": t.name,
"tool": t.action.params.get("tool"),
"cron": None if t.manual else t.cron,
"enabled": t.enabled,
"native_command": _native_command_for(t.action.params.get("tool")),
}
for t in affected
],
},
}

def _check_config_source(self) -> dict[str, Any]:
"""Check 0: Report which config source is active."""
return {
Expand Down
Loading
Loading