feat: add list_applications tool for YARN application enumeration
Add a new MCP tool that queries YARN's /ws/v1/cluster/apps endpoint
through a named Connection, returning a list of ApplicationSummary
records. Bypasses the local JobStore — useful for enumerating apps
that were not submitted through this service.
API:
list_applications(
connection_name: str, # required, which YARN cluster
state: str | None = None, # YARN state filter: NEW/NEW_SAVING/
# SUBMITTED/ACCEPTED/RUNNING/
# FINISHED/FAILED/KILLED
queue: str | None = None, # YARN queue filter
limit: int = 100, # cap on returned apps (YARN has no
# offset-based pagination; combine
# state/queue filters for big clusters)
) -> list[ApplicationSummary]
Implementation:
- yarn_client.list_applications(config, *, state, queue, limit) -> list[dict]
Returns raw YARN app dicts; raises YarnError on 4xx/5xx; returns
[] on 404 (no apps match). Uses the existing _request helper,
which now accepts a "params" kwarg for query strings (one-line
additive change).
- external_jobs.list_applications(connection_name, state, queue, limit)
-> list[ApplicationSummary]. Looks up the Connection, builds the
YarnClientConfig, calls the yarn_client function, maps each raw
YARN dict to ApplicationSummary (mirroring the manual field-mapping
style of get_job_result). The yarn_client function is imported
as "list_applications_yarn" to avoid name collision.
- ApplicationSummary: 12-field Pydantic model with snake_case names
(application_id, name, user, queue, state, final_status,
application_type, application_tags, started_time, finished_time,
tracking_url, progress). Unused YARN fields (memorySeconds,
vcoreSeconds, preemptedResource*, etc.) are not exposed.
- ListApplicationsRequest: Pydantic body model with Field(description=)
for LLM-facing schema.
- /list_applications route registered with operation_id=
"list_applications", placed next to the other external YARN tools.
Tests:
- 8 new unit tests in test_external_jobs.py (happy path, state/queue/
limit pass-through, default limit, empty list, missing connection,
full field mapping).
- test_mcp_routes.py: assert 23 tool routes.
- README: list_applications row added to the Spark Executor table.
Tests: 390 passed (was 382, +8 net).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -121,13 +121,14 @@ def _base_url(yarn_rm_url: str | None) -> str:
|
||||
|
||||
|
||||
def _request(method: str, url: str, *, json_body: dict | None = None,
|
||||
timeout: float = 30.0, verify: bool | str = True,
|
||||
auth: httpx.Auth | None = None) -> httpx.Response:
|
||||
params: dict[str, str] | None = None, timeout: float = 30.0,
|
||||
verify: bool | str = True, auth: httpx.Auth | None = None) -> httpx.Response:
|
||||
headers = {"Accept": "application/json"}
|
||||
logger.debug(f"YARN {method} {url}" + (f" body={json_body}" if json_body else ""))
|
||||
try:
|
||||
resp = httpx.request(
|
||||
method, url, json=json_body, headers=headers, timeout=timeout, verify=verify, auth=auth
|
||||
method, url, json=json_body, params=params, headers=headers,
|
||||
timeout=timeout, verify=verify, auth=auth
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
logger.error(f"YARN {method} {url} failed: {exc}")
|
||||
@@ -257,3 +258,54 @@ def kill_application(application_id: str, config: YarnClientConfig) -> None:
|
||||
logger.error(f"YARN PUT {url} -> {resp.status_code}: {resp.text[:500]}")
|
||||
raise YarnError(f"YARN kill returned HTTP {resp.status_code}: {resp.text}")
|
||||
logger.info(f"YARN kill {application_id} -> ok")
|
||||
|
||||
|
||||
def list_applications(
|
||||
config: YarnClientConfig,
|
||||
*,
|
||||
state: str | None = None,
|
||||
queue: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> list[dict]:
|
||||
"""List YARN applications, optionally filtered.
|
||||
|
||||
YARN endpoint: GET /ws/v1/cluster/apps?state=...&queue=...&limit=...
|
||||
|
||||
Filters:
|
||||
- state: YARN application state. Common values:
|
||||
"NEW", "NEW_SAVING", "SUBMITTED", "ACCEPTED", "RUNNING",
|
||||
"FINISHED", "FAILED", "KILLED".
|
||||
Note: "FINISHED" is the umbrella state covering SUCCEEDED/FAILED/KILLED.
|
||||
- queue: YARN queue name
|
||||
- limit: cap on number of returned apps (YARN has no pagination;
|
||||
callers that need a full enumeration should make multiple
|
||||
calls with state=... filters or accept the cap)
|
||||
|
||||
Returns a list of YARN app dicts (each with id, name, user, queue,
|
||||
state, finalStatus, applicationType, startedTime, finishedTime,
|
||||
trackingUrl, progress, etc). Empty list if no apps match.
|
||||
|
||||
Raises YarnError on transport / 4xx / 5xx.
|
||||
"""
|
||||
params: dict[str, str] = {}
|
||||
if state is not None:
|
||||
params["state"] = state
|
||||
if queue is not None:
|
||||
params["queue"] = queue
|
||||
if limit is not None:
|
||||
params["limit"] = str(limit)
|
||||
|
||||
url = f"{_base_url(config.yarn_rm_url)}/ws/v1/cluster/apps"
|
||||
resp = _request("GET", url, params=params,
|
||||
verify=config.verify_for_httpx(),
|
||||
auth=config.auth_for_httpx())
|
||||
if resp.status_code == 404:
|
||||
# No apps match (or RM doesn't support the endpoint)
|
||||
return []
|
||||
if resp.status_code >= 400:
|
||||
raise YarnError(
|
||||
f"YARN list applications failed: {resp.status_code} {resp.text[:200]}"
|
||||
)
|
||||
data = resp.json()
|
||||
apps_container = data.get("apps") or {}
|
||||
return apps_container.get("app", []) or []
|
||||
|
||||
Reference in New Issue
Block a user