databricks-mcp-toolkit 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- databricks_mcp/__init__.py +10 -0
- databricks_mcp/__main__.py +10 -0
- databricks_mcp/api/__init__.py +30 -0
- databricks_mcp/api/clusters.py +73 -0
- databricks_mcp/api/command_execution.py +92 -0
- databricks_mcp/api/dbfs.py +37 -0
- databricks_mcp/api/files.py +94 -0
- databricks_mcp/api/jobs.py +144 -0
- databricks_mcp/api/sql.py +99 -0
- databricks_mcp/api/unity_catalog.py +76 -0
- databricks_mcp/api/warehouses.py +32 -0
- databricks_mcp/api/workspace.py +67 -0
- databricks_mcp/cli.py +100 -0
- databricks_mcp/client.py +208 -0
- databricks_mcp/config.py +56 -0
- databricks_mcp/server.py +642 -0
- databricks_mcp_toolkit-0.2.0.dist-info/METADATA +282 -0
- databricks_mcp_toolkit-0.2.0.dist-info/RECORD +21 -0
- databricks_mcp_toolkit-0.2.0.dist-info/WHEEL +4 -0
- databricks_mcp_toolkit-0.2.0.dist-info/entry_points.txt +2 -0
- databricks_mcp_toolkit-0.2.0.dist-info/licenses/LICENSE +661 -0
databricks_mcp/server.py
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Kush Patel
|
|
2
|
+
# Dual-licensed: AGPL-3.0-only OR commercial license.
|
|
3
|
+
# If you obtained this from GitHub or PyPI, you may use it under the AGPL-3.0 terms.
|
|
4
|
+
# For commercial terms, see the project README.
|
|
5
|
+
|
|
6
|
+
"""MCP server: registers one tool per Databricks operation."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
|
13
|
+
from contextlib import asynccontextmanager
|
|
14
|
+
from typing import Annotated, Any, Literal
|
|
15
|
+
|
|
16
|
+
from mcp.server.mcpserver import MCPServer
|
|
17
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
18
|
+
from mcp.types import ToolAnnotations
|
|
19
|
+
from pydantic import Field
|
|
20
|
+
|
|
21
|
+
from databricks_mcp import __version__
|
|
22
|
+
from databricks_mcp.api import (
|
|
23
|
+
clusters,
|
|
24
|
+
command_execution,
|
|
25
|
+
dbfs,
|
|
26
|
+
files,
|
|
27
|
+
jobs,
|
|
28
|
+
sql,
|
|
29
|
+
unity_catalog,
|
|
30
|
+
warehouses,
|
|
31
|
+
workspace,
|
|
32
|
+
)
|
|
33
|
+
from databricks_mcp.client import DatabricksAPIError, DatabricksClient
|
|
34
|
+
from databricks_mcp.config import Settings
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
TOOLSETS = ("clusters", "jobs", "workspace", "volumes", "dbfs", "sql", "unity_catalog", "commands")
|
|
39
|
+
|
|
40
|
+
INSTRUCTIONS = """\
|
|
41
|
+
Tools for a Databricks workspace. Tips:
|
|
42
|
+
- Explore data with list_catalogs -> list_schemas -> list_tables -> get_table, then query with execute_sql.
|
|
43
|
+
- execute_sql needs a SQL warehouse; use list_sql_warehouses if no default is configured.
|
|
44
|
+
- Files in Unity Catalog volumes live under /Volumes/<catalog>/<schema>/<volume>/.
|
|
45
|
+
- Long-running work (job runs, SQL, commands) returns an id; poll it rather than resubmitting.
|
|
46
|
+
- List tools return a page plus next_page_token when there is more.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
Language = Literal["python", "scala", "sql", "r"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _ann(read_only: bool, destructive: bool = False, idempotent: bool = False) -> ToolAnnotations:
|
|
53
|
+
return ToolAnnotations(
|
|
54
|
+
read_only_hint=read_only,
|
|
55
|
+
destructive_hint=destructive if not read_only else False,
|
|
56
|
+
idempotent_hint=idempotent or read_only,
|
|
57
|
+
open_world_hint=True,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _slim(items: Iterable[dict[str, Any]], keys: tuple[str, ...]) -> list[dict[str, Any]]:
|
|
62
|
+
return [{k: item[k] for k in keys if k in item} for item in items]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_server(
|
|
66
|
+
client: DatabricksClient | None = None,
|
|
67
|
+
settings: Settings | None = None,
|
|
68
|
+
toolsets: Iterable[str] | None = None,
|
|
69
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "WARNING",
|
|
70
|
+
) -> MCPServer:
|
|
71
|
+
settings = settings or Settings.from_env()
|
|
72
|
+
client = client or DatabricksClient(timeout_seconds=settings.http_timeout_seconds, max_retries=settings.max_retries)
|
|
73
|
+
enabled = set(toolsets) if toolsets is not None else set(TOOLSETS)
|
|
74
|
+
unknown = enabled - set(TOOLSETS)
|
|
75
|
+
if unknown:
|
|
76
|
+
raise ValueError(f"Unknown toolset(s): {sorted(unknown)}. Valid: {', '.join(TOOLSETS)}")
|
|
77
|
+
|
|
78
|
+
@asynccontextmanager
|
|
79
|
+
async def lifespan(_server: MCPServer) -> AsyncIterator[None]:
|
|
80
|
+
try:
|
|
81
|
+
yield
|
|
82
|
+
finally:
|
|
83
|
+
await client.aclose()
|
|
84
|
+
|
|
85
|
+
mode = " (read-only mode: write tools are disabled)" if settings.read_only else ""
|
|
86
|
+
server = MCPServer(
|
|
87
|
+
name="databricks-mcp",
|
|
88
|
+
version=__version__,
|
|
89
|
+
instructions=INSTRUCTIONS + mode,
|
|
90
|
+
website_url="https://github.com/kshptl/Databricks-MCP-Server",
|
|
91
|
+
lifespan=lifespan,
|
|
92
|
+
log_level=log_level,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def dump(result: Any) -> str:
|
|
96
|
+
text = json.dumps(result, default=str, ensure_ascii=False)
|
|
97
|
+
limit = settings.max_response_chars
|
|
98
|
+
if len(text) > limit:
|
|
99
|
+
return (
|
|
100
|
+
text[:limit] + f"\n...[truncated {len(text) - limit} characters. Narrow the request: use "
|
|
101
|
+
"limit/max_results, page_token, filters, or a more specific path.]"
|
|
102
|
+
)
|
|
103
|
+
return text
|
|
104
|
+
|
|
105
|
+
async def run(call: Awaitable[Any]) -> str:
|
|
106
|
+
# Expected failures become ToolError so the model sees the actual message
|
|
107
|
+
# (the SDK hides the text of any other exception).
|
|
108
|
+
try:
|
|
109
|
+
return dump(await call)
|
|
110
|
+
except DatabricksAPIError as exc:
|
|
111
|
+
raise ToolError(str(exc)) from exc
|
|
112
|
+
except ValueError as exc:
|
|
113
|
+
raise ToolError(str(exc)) from exc
|
|
114
|
+
|
|
115
|
+
def tool(
|
|
116
|
+
toolset: str, read_only: bool, destructive: bool = False, idempotent: bool = False
|
|
117
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
118
|
+
def register(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
119
|
+
if toolset not in enabled or (settings.read_only and not read_only):
|
|
120
|
+
return fn
|
|
121
|
+
return server.tool(annotations=_ann(read_only, destructive, idempotent))(fn)
|
|
122
|
+
|
|
123
|
+
return register
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------- clusters
|
|
126
|
+
|
|
127
|
+
@tool("clusters", read_only=True)
|
|
128
|
+
async def list_clusters(
|
|
129
|
+
full: Annotated[bool, Field(description="Return the full cluster objects instead of a summary.")] = False,
|
|
130
|
+
page_token: str | None = None,
|
|
131
|
+
) -> str:
|
|
132
|
+
"""List pinned and active clusters, plus clusters terminated in the last 30 days."""
|
|
133
|
+
|
|
134
|
+
async def call() -> Any:
|
|
135
|
+
result = await clusters.list_clusters(client, page_size=100, page_token=page_token)
|
|
136
|
+
if not full:
|
|
137
|
+
result["clusters"] = _slim(
|
|
138
|
+
result.get("clusters", []),
|
|
139
|
+
(
|
|
140
|
+
"cluster_id",
|
|
141
|
+
"cluster_name",
|
|
142
|
+
"state",
|
|
143
|
+
"spark_version",
|
|
144
|
+
"node_type_id",
|
|
145
|
+
"num_workers",
|
|
146
|
+
"autoscale",
|
|
147
|
+
"creator_user_name",
|
|
148
|
+
"cluster_source",
|
|
149
|
+
"autotermination_minutes",
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
return await run(call())
|
|
155
|
+
|
|
156
|
+
@tool("clusters", read_only=True)
|
|
157
|
+
async def get_cluster(cluster_id: str) -> str:
|
|
158
|
+
"""Get the full configuration and state of one cluster."""
|
|
159
|
+
return await run(clusters.get_cluster(client, cluster_id))
|
|
160
|
+
|
|
161
|
+
@tool("clusters", read_only=True)
|
|
162
|
+
async def list_spark_versions() -> str:
|
|
163
|
+
"""List Databricks Runtime versions (spark_version keys) available for new clusters."""
|
|
164
|
+
return await run(clusters.list_spark_versions(client))
|
|
165
|
+
|
|
166
|
+
@tool("clusters", read_only=True)
|
|
167
|
+
async def list_node_types() -> str:
|
|
168
|
+
"""List instance types (node_type_id values) available in this workspace."""
|
|
169
|
+
|
|
170
|
+
async def call() -> Any:
|
|
171
|
+
result = await clusters.list_node_types(client)
|
|
172
|
+
result["node_types"] = _slim(
|
|
173
|
+
result.get("node_types", []),
|
|
174
|
+
("node_type_id", "memory_mb", "num_cores", "num_gpus", "category", "is_deprecated"),
|
|
175
|
+
)
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
return await run(call())
|
|
179
|
+
|
|
180
|
+
@tool("clusters", read_only=False)
|
|
181
|
+
async def create_cluster(
|
|
182
|
+
cluster_name: str,
|
|
183
|
+
spark_version: Annotated[
|
|
184
|
+
str, Field(description="Runtime key from list_spark_versions, e.g. '16.4.x-scala2.12'.")
|
|
185
|
+
],
|
|
186
|
+
node_type_id: Annotated[str, Field(description="Instance type from list_node_types.")],
|
|
187
|
+
num_workers: Annotated[int | None, Field(description="Fixed worker count. Omit if using autoscale.")] = None,
|
|
188
|
+
autoscale_min_workers: int | None = None,
|
|
189
|
+
autoscale_max_workers: int | None = None,
|
|
190
|
+
autotermination_minutes: Annotated[
|
|
191
|
+
int, Field(description="Idle minutes before auto-terminate. 0 disables it.")
|
|
192
|
+
] = 60,
|
|
193
|
+
extra_spec: Annotated[
|
|
194
|
+
dict[str, Any] | None,
|
|
195
|
+
Field(
|
|
196
|
+
description="Any other Clusters API create fields, e.g. spark_conf, custom_tags, data_security_mode."
|
|
197
|
+
),
|
|
198
|
+
] = None,
|
|
199
|
+
) -> str:
|
|
200
|
+
"""Create an all-purpose cluster. This starts billable compute."""
|
|
201
|
+
spec: dict[str, Any] = {
|
|
202
|
+
**(extra_spec or {}),
|
|
203
|
+
"cluster_name": cluster_name,
|
|
204
|
+
"spark_version": spark_version,
|
|
205
|
+
"node_type_id": node_type_id,
|
|
206
|
+
"autotermination_minutes": autotermination_minutes,
|
|
207
|
+
}
|
|
208
|
+
if autoscale_min_workers is not None and autoscale_max_workers is not None:
|
|
209
|
+
spec["autoscale"] = {"min_workers": autoscale_min_workers, "max_workers": autoscale_max_workers}
|
|
210
|
+
else:
|
|
211
|
+
spec["num_workers"] = num_workers or 0
|
|
212
|
+
return await run(clusters.create_cluster(client, spec))
|
|
213
|
+
|
|
214
|
+
@tool("clusters", read_only=False, idempotent=True)
|
|
215
|
+
async def start_cluster(cluster_id: str) -> str:
|
|
216
|
+
"""Start a terminated cluster."""
|
|
217
|
+
return await run(clusters.start_cluster(client, cluster_id))
|
|
218
|
+
|
|
219
|
+
@tool("clusters", read_only=False, destructive=True)
|
|
220
|
+
async def restart_cluster(cluster_id: str) -> str:
|
|
221
|
+
"""Restart a running cluster. Running commands and notebook state are lost."""
|
|
222
|
+
return await run(clusters.restart_cluster(client, cluster_id))
|
|
223
|
+
|
|
224
|
+
@tool("clusters", read_only=False, idempotent=True)
|
|
225
|
+
async def resize_cluster(
|
|
226
|
+
cluster_id: str,
|
|
227
|
+
num_workers: int | None = None,
|
|
228
|
+
autoscale_min_workers: int | None = None,
|
|
229
|
+
autoscale_max_workers: int | None = None,
|
|
230
|
+
) -> str:
|
|
231
|
+
"""Resize a running cluster to a fixed worker count or an autoscale range."""
|
|
232
|
+
return await run(
|
|
233
|
+
clusters.resize_cluster(client, cluster_id, num_workers, autoscale_min_workers, autoscale_max_workers)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@tool("clusters", read_only=False, destructive=True, idempotent=True)
|
|
237
|
+
async def terminate_cluster(cluster_id: str) -> str:
|
|
238
|
+
"""Terminate (stop) a cluster. Its config is kept and it can be started again."""
|
|
239
|
+
return await run(clusters.terminate_cluster(client, cluster_id))
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------- jobs
|
|
242
|
+
|
|
243
|
+
@tool("jobs", read_only=True)
|
|
244
|
+
async def list_jobs(
|
|
245
|
+
name: Annotated[str | None, Field(description="Exact job name filter (case-insensitive).")] = None,
|
|
246
|
+
limit: Annotated[int, Field(ge=1, le=100)] = 25,
|
|
247
|
+
page_token: str | None = None,
|
|
248
|
+
) -> str:
|
|
249
|
+
"""List jobs (summary: id, name, creator, created time)."""
|
|
250
|
+
|
|
251
|
+
async def call() -> Any:
|
|
252
|
+
result = await jobs.list_jobs(client, name=name, limit=limit, page_token=page_token)
|
|
253
|
+
result["jobs"] = [
|
|
254
|
+
{
|
|
255
|
+
"job_id": j.get("job_id"),
|
|
256
|
+
"name": (j.get("settings") or {}).get("name"),
|
|
257
|
+
"creator_user_name": j.get("creator_user_name"),
|
|
258
|
+
"created_time": j.get("created_time"),
|
|
259
|
+
}
|
|
260
|
+
for j in result.get("jobs", [])
|
|
261
|
+
]
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
return await run(call())
|
|
265
|
+
|
|
266
|
+
@tool("jobs", read_only=True)
|
|
267
|
+
async def get_job(
|
|
268
|
+
job_id: int,
|
|
269
|
+
page_token: Annotated[str | None, Field(description="For jobs with more than 100 tasks.")] = None,
|
|
270
|
+
) -> str:
|
|
271
|
+
"""Get a job's full settings (tasks, clusters, schedule, parameters)."""
|
|
272
|
+
return await run(jobs.get_job(client, job_id, page_token))
|
|
273
|
+
|
|
274
|
+
@tool("jobs", read_only=True)
|
|
275
|
+
async def list_job_runs(
|
|
276
|
+
job_id: Annotated[int | None, Field(description="Omit to list runs across all jobs.")] = None,
|
|
277
|
+
active_only: bool = False,
|
|
278
|
+
completed_only: bool = False,
|
|
279
|
+
limit: Annotated[int, Field(ge=1, le=25)] = 25,
|
|
280
|
+
page_token: str | None = None,
|
|
281
|
+
) -> str:
|
|
282
|
+
"""List job runs, newest first."""
|
|
283
|
+
|
|
284
|
+
async def call() -> Any:
|
|
285
|
+
result = await jobs.list_runs(client, job_id, active_only, completed_only, limit, page_token)
|
|
286
|
+
result["runs"] = _slim(
|
|
287
|
+
result.get("runs", []),
|
|
288
|
+
(
|
|
289
|
+
"run_id",
|
|
290
|
+
"job_id",
|
|
291
|
+
"run_name",
|
|
292
|
+
"status",
|
|
293
|
+
"state",
|
|
294
|
+
"start_time",
|
|
295
|
+
"end_time",
|
|
296
|
+
"run_duration",
|
|
297
|
+
"trigger",
|
|
298
|
+
"run_page_url",
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
return await run(call())
|
|
304
|
+
|
|
305
|
+
@tool("jobs", read_only=True)
|
|
306
|
+
async def get_run(run_id: int, page_token: str | None = None) -> str:
|
|
307
|
+
"""Get a run's details, including per-task state."""
|
|
308
|
+
return await run(jobs.get_run(client, run_id, page_token))
|
|
309
|
+
|
|
310
|
+
@tool("jobs", read_only=True)
|
|
311
|
+
async def get_run_output(run_id: int) -> str:
|
|
312
|
+
"""Get a run's output: notebook exit value, logs, error and trace. For a multi-task
|
|
313
|
+
run, returns each task's output."""
|
|
314
|
+
return await run(jobs.get_run_output(client, run_id))
|
|
315
|
+
|
|
316
|
+
@tool("jobs", read_only=True)
|
|
317
|
+
async def wait_for_run(
|
|
318
|
+
run_id: int,
|
|
319
|
+
timeout_seconds: Annotated[int, Field(ge=1, le=1800)] = 600,
|
|
320
|
+
poll_interval_seconds: Annotated[int, Field(ge=1, le=300)] = 10,
|
|
321
|
+
) -> str:
|
|
322
|
+
"""Poll a run until it finishes or the timeout passes. Returns its final state."""
|
|
323
|
+
return await run(jobs.wait_for_run(client, run_id, poll_interval_seconds, timeout_seconds))
|
|
324
|
+
|
|
325
|
+
@tool("jobs", read_only=False)
|
|
326
|
+
async def run_job(
|
|
327
|
+
job_id: int,
|
|
328
|
+
job_parameters: Annotated[
|
|
329
|
+
dict[str, str] | None, Field(description="Job-level parameters (preferred for jobs that define them).")
|
|
330
|
+
] = None,
|
|
331
|
+
notebook_params: Annotated[
|
|
332
|
+
dict[str, str] | None, Field(description="Legacy notebook widget params for older jobs.")
|
|
333
|
+
] = None,
|
|
334
|
+
) -> str:
|
|
335
|
+
"""Trigger a run of an existing job. Returns run_id."""
|
|
336
|
+
return await run(jobs.run_job(client, job_id, job_parameters, notebook_params))
|
|
337
|
+
|
|
338
|
+
@tool("jobs", read_only=False)
|
|
339
|
+
async def submit_run(
|
|
340
|
+
run_spec: Annotated[
|
|
341
|
+
dict[str, Any],
|
|
342
|
+
Field(
|
|
343
|
+
description=(
|
|
344
|
+
"Jobs API runs/submit body. Needs 'tasks': a list of {task_key, notebook_task | "
|
|
345
|
+
"spark_python_task | sql_task | ..., plus existing_cluster_id, new_cluster, or "
|
|
346
|
+
"environment_key for serverless}. Optional: run_name, timeout_seconds, git_source."
|
|
347
|
+
)
|
|
348
|
+
),
|
|
349
|
+
],
|
|
350
|
+
) -> str:
|
|
351
|
+
"""Submit a one-time run without creating a job. Returns run_id."""
|
|
352
|
+
return await run(jobs.submit_run(client, run_spec))
|
|
353
|
+
|
|
354
|
+
@tool("jobs", read_only=False, destructive=True, idempotent=True)
|
|
355
|
+
async def cancel_run(run_id: int) -> str:
|
|
356
|
+
"""Cancel an active run."""
|
|
357
|
+
return await run(jobs.cancel_run(client, run_id))
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------- workspace
|
|
360
|
+
|
|
361
|
+
@tool("workspace", read_only=True)
|
|
362
|
+
async def list_workspace(
|
|
363
|
+
path: Annotated[str, Field(description="Workspace folder, e.g. /Users/me@example.com or /Shared.")],
|
|
364
|
+
) -> str:
|
|
365
|
+
"""List notebooks, files and folders in a workspace directory."""
|
|
366
|
+
return await run(workspace.list_objects(client, path))
|
|
367
|
+
|
|
368
|
+
@tool("workspace", read_only=True)
|
|
369
|
+
async def get_workspace_object(path: str) -> str:
|
|
370
|
+
"""Get metadata (type, language, ids) for a workspace path."""
|
|
371
|
+
return await run(workspace.get_status(client, path))
|
|
372
|
+
|
|
373
|
+
@tool("workspace", read_only=True)
|
|
374
|
+
async def export_notebook(
|
|
375
|
+
path: str,
|
|
376
|
+
format: Literal["SOURCE", "JUPYTER", "HTML"] = "SOURCE",
|
|
377
|
+
) -> str:
|
|
378
|
+
"""Export a notebook's contents as text."""
|
|
379
|
+
return await run(workspace.export_notebook(client, path, format))
|
|
380
|
+
|
|
381
|
+
@tool("workspace", read_only=False, destructive=True)
|
|
382
|
+
async def import_notebook(
|
|
383
|
+
path: str,
|
|
384
|
+
content: Annotated[str, Field(description="Notebook source as plain text (not base64).")],
|
|
385
|
+
language: Language | None = None,
|
|
386
|
+
format: Literal["SOURCE", "JUPYTER"] = "SOURCE",
|
|
387
|
+
overwrite: bool = False,
|
|
388
|
+
) -> str:
|
|
389
|
+
"""Create or (with overwrite=true) replace a notebook. language is required for SOURCE."""
|
|
390
|
+
lang = language.upper() if language else None
|
|
391
|
+
return await run(workspace.import_notebook(client, path, content, lang, format, overwrite))
|
|
392
|
+
|
|
393
|
+
@tool("workspace", read_only=False, idempotent=True)
|
|
394
|
+
async def create_workspace_directory(path: str) -> str:
|
|
395
|
+
"""Create a workspace folder, including missing parents."""
|
|
396
|
+
return await run(workspace.mkdirs(client, path))
|
|
397
|
+
|
|
398
|
+
# ---------------------------------------------------------------- volumes (Files API)
|
|
399
|
+
|
|
400
|
+
@tool("volumes", read_only=True)
|
|
401
|
+
async def list_volume_directory(
|
|
402
|
+
path: Annotated[str, Field(description="e.g. /Volumes/main/default/raw/2026/")],
|
|
403
|
+
page_token: str | None = None,
|
|
404
|
+
) -> str:
|
|
405
|
+
"""List files and folders in a Unity Catalog volume directory."""
|
|
406
|
+
return await run(files.list_directory(client, path, page_size=500, page_token=page_token))
|
|
407
|
+
|
|
408
|
+
@tool("volumes", read_only=True)
|
|
409
|
+
async def read_volume_file(
|
|
410
|
+
path: str,
|
|
411
|
+
max_bytes: Annotated[int, Field(ge=1, le=1_000_000)] = 100_000,
|
|
412
|
+
) -> str:
|
|
413
|
+
"""Read the start of a text file in a volume (up to max_bytes)."""
|
|
414
|
+
return await run(files.read_file(client, path, max_bytes))
|
|
415
|
+
|
|
416
|
+
@tool("volumes", read_only=False, destructive=True, idempotent=True)
|
|
417
|
+
async def write_volume_file(path: str, content: str, overwrite: bool = False) -> str:
|
|
418
|
+
"""Write a text file to a volume. Fails if it exists unless overwrite=true."""
|
|
419
|
+
return await run(files.write_file(client, path, content, overwrite))
|
|
420
|
+
|
|
421
|
+
@tool("volumes", read_only=False, destructive=True, idempotent=True)
|
|
422
|
+
async def delete_volume_file(path: str) -> str:
|
|
423
|
+
"""Delete a file from a volume."""
|
|
424
|
+
return await run(files.delete_file(client, path))
|
|
425
|
+
|
|
426
|
+
@tool("volumes", read_only=False, idempotent=True)
|
|
427
|
+
async def create_volume_directory(path: str) -> str:
|
|
428
|
+
"""Create a directory in a volume, including missing parents."""
|
|
429
|
+
return await run(files.create_directory(client, path))
|
|
430
|
+
|
|
431
|
+
# ---------------------------------------------------------------- dbfs (legacy)
|
|
432
|
+
|
|
433
|
+
@tool("dbfs", read_only=True)
|
|
434
|
+
async def list_dbfs(path: Annotated[str, Field(description="DBFS path, e.g. /FileStore or dbfs:/tmp.")]) -> str:
|
|
435
|
+
"""List a legacy DBFS path. Prefer volumes for new data."""
|
|
436
|
+
return await run(dbfs.list_files(client, path))
|
|
437
|
+
|
|
438
|
+
@tool("dbfs", read_only=True)
|
|
439
|
+
async def read_dbfs_file(path: str, max_bytes: Annotated[int, Field(ge=1, le=1_048_576)] = 100_000) -> str:
|
|
440
|
+
"""Read the start of a text file on legacy DBFS."""
|
|
441
|
+
return await run(dbfs.read_file(client, path, max_bytes))
|
|
442
|
+
|
|
443
|
+
# ---------------------------------------------------------------- sql
|
|
444
|
+
|
|
445
|
+
@tool("sql", read_only=True)
|
|
446
|
+
async def list_sql_warehouses() -> str:
|
|
447
|
+
"""List SQL warehouses with their id, state and size."""
|
|
448
|
+
|
|
449
|
+
async def call() -> Any:
|
|
450
|
+
result = await warehouses.list_warehouses(client)
|
|
451
|
+
result["warehouses"] = _slim(
|
|
452
|
+
result.get("warehouses", []),
|
|
453
|
+
(
|
|
454
|
+
"id",
|
|
455
|
+
"name",
|
|
456
|
+
"state",
|
|
457
|
+
"cluster_size",
|
|
458
|
+
"warehouse_type",
|
|
459
|
+
"enable_serverless_compute",
|
|
460
|
+
"auto_stop_mins",
|
|
461
|
+
"creator_name",
|
|
462
|
+
),
|
|
463
|
+
)
|
|
464
|
+
return result
|
|
465
|
+
|
|
466
|
+
return await run(call())
|
|
467
|
+
|
|
468
|
+
@tool("sql", read_only=True)
|
|
469
|
+
async def get_sql_warehouse(warehouse_id: str) -> str:
|
|
470
|
+
"""Get one SQL warehouse's full configuration and state."""
|
|
471
|
+
return await run(warehouses.get_warehouse(client, warehouse_id))
|
|
472
|
+
|
|
473
|
+
@tool("sql", read_only=False, idempotent=True)
|
|
474
|
+
async def start_sql_warehouse(warehouse_id: str) -> str:
|
|
475
|
+
"""Start a stopped SQL warehouse."""
|
|
476
|
+
return await run(warehouses.start_warehouse(client, warehouse_id))
|
|
477
|
+
|
|
478
|
+
@tool("sql", read_only=False, destructive=True, idempotent=True)
|
|
479
|
+
async def stop_sql_warehouse(warehouse_id: str) -> str:
|
|
480
|
+
"""Stop a SQL warehouse. Running queries on it are cancelled."""
|
|
481
|
+
return await run(warehouses.stop_warehouse(client, warehouse_id))
|
|
482
|
+
|
|
483
|
+
@tool("sql", read_only=False, destructive=True)
|
|
484
|
+
async def execute_sql(
|
|
485
|
+
statement: str,
|
|
486
|
+
warehouse_id: Annotated[
|
|
487
|
+
str | None, Field(description="Defaults to DATABRICKS_WAREHOUSE_ID. See list_sql_warehouses.")
|
|
488
|
+
] = None,
|
|
489
|
+
catalog: str | None = None,
|
|
490
|
+
schema: str | None = None,
|
|
491
|
+
parameters: Annotated[
|
|
492
|
+
dict[str, str] | None, Field(description="Named parameters, referenced as :name in the statement.")
|
|
493
|
+
] = None,
|
|
494
|
+
row_limit: Annotated[int, Field(ge=1, le=10_000)] = 1000,
|
|
495
|
+
timeout_seconds: Annotated[int, Field(ge=5, le=900)] = 120,
|
|
496
|
+
) -> str:
|
|
497
|
+
"""Run a SQL statement on a SQL warehouse and return columns + rows. If it outlives
|
|
498
|
+
timeout_seconds you get a statement_id to poll with get_sql_statement."""
|
|
499
|
+
target = warehouse_id or settings.warehouse_id
|
|
500
|
+
if not target:
|
|
501
|
+
raise ToolError("No warehouse_id given and DATABRICKS_WAREHOUSE_ID is not set. Call list_sql_warehouses.")
|
|
502
|
+
return await run(
|
|
503
|
+
sql.execute(client, statement, target, catalog, schema, parameters, row_limit, timeout_seconds)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
@tool("sql", read_only=True)
|
|
507
|
+
async def get_sql_statement(statement_id: str) -> str:
|
|
508
|
+
"""Get the state and (if finished) results of a SQL statement."""
|
|
509
|
+
return await run(sql.get_statement(client, statement_id))
|
|
510
|
+
|
|
511
|
+
@tool("sql", read_only=False, idempotent=True)
|
|
512
|
+
async def cancel_sql_statement(statement_id: str) -> str:
|
|
513
|
+
"""Cancel a running SQL statement."""
|
|
514
|
+
return await run(sql.cancel_statement(client, statement_id))
|
|
515
|
+
|
|
516
|
+
# ---------------------------------------------------------------- unity catalog
|
|
517
|
+
|
|
518
|
+
@tool("unity_catalog", read_only=True)
|
|
519
|
+
async def list_catalogs(
|
|
520
|
+
max_results: Annotated[int, Field(ge=1, le=1000)] = 100, page_token: str | None = None
|
|
521
|
+
) -> str:
|
|
522
|
+
"""List Unity Catalog catalogs you can see."""
|
|
523
|
+
|
|
524
|
+
async def call() -> Any:
|
|
525
|
+
result = await unity_catalog.list_catalogs(client, max_results, page_token)
|
|
526
|
+
result["catalogs"] = _slim(result.get("catalogs", []), ("name", "catalog_type", "comment", "owner"))
|
|
527
|
+
return result
|
|
528
|
+
|
|
529
|
+
return await run(call())
|
|
530
|
+
|
|
531
|
+
@tool("unity_catalog", read_only=True)
|
|
532
|
+
async def list_schemas(
|
|
533
|
+
catalog_name: str, max_results: Annotated[int, Field(ge=1, le=1000)] = 100, page_token: str | None = None
|
|
534
|
+
) -> str:
|
|
535
|
+
"""List schemas in a catalog."""
|
|
536
|
+
|
|
537
|
+
async def call() -> Any:
|
|
538
|
+
result = await unity_catalog.list_schemas(client, catalog_name, max_results, page_token)
|
|
539
|
+
result["schemas"] = _slim(result.get("schemas", []), ("name", "full_name", "comment", "owner"))
|
|
540
|
+
return result
|
|
541
|
+
|
|
542
|
+
return await run(call())
|
|
543
|
+
|
|
544
|
+
@tool("unity_catalog", read_only=True)
|
|
545
|
+
async def list_tables(
|
|
546
|
+
catalog_name: str,
|
|
547
|
+
schema_name: str,
|
|
548
|
+
max_results: Annotated[int, Field(ge=1, le=1000)] = 100,
|
|
549
|
+
page_token: str | None = None,
|
|
550
|
+
) -> str:
|
|
551
|
+
"""List tables and views in a schema (names and types; use get_table for columns)."""
|
|
552
|
+
|
|
553
|
+
async def call() -> Any:
|
|
554
|
+
result = await unity_catalog.list_tables(client, catalog_name, schema_name, max_results, page_token)
|
|
555
|
+
result["tables"] = _slim(
|
|
556
|
+
result.get("tables", []),
|
|
557
|
+
("name", "full_name", "table_type", "data_source_format", "comment", "updated_at"),
|
|
558
|
+
)
|
|
559
|
+
return result
|
|
560
|
+
|
|
561
|
+
return await run(call())
|
|
562
|
+
|
|
563
|
+
@tool("unity_catalog", read_only=True)
|
|
564
|
+
async def get_table(
|
|
565
|
+
full_name: Annotated[str, Field(description="catalog.schema.table")],
|
|
566
|
+
) -> str:
|
|
567
|
+
"""Get a table's columns, types, comments, format and storage location."""
|
|
568
|
+
|
|
569
|
+
async def call() -> Any:
|
|
570
|
+
table = await unity_catalog.get_table(client, full_name)
|
|
571
|
+
table["columns"] = _slim(
|
|
572
|
+
table.get("columns", []), ("name", "type_text", "nullable", "comment", "partition_index")
|
|
573
|
+
)
|
|
574
|
+
return table
|
|
575
|
+
|
|
576
|
+
return await run(call())
|
|
577
|
+
|
|
578
|
+
@tool("unity_catalog", read_only=True)
|
|
579
|
+
async def list_volumes(
|
|
580
|
+
catalog_name: str,
|
|
581
|
+
schema_name: str,
|
|
582
|
+
max_results: Annotated[int, Field(ge=1, le=1000)] = 100,
|
|
583
|
+
page_token: str | None = None,
|
|
584
|
+
) -> str:
|
|
585
|
+
"""List Unity Catalog volumes in a schema."""
|
|
586
|
+
return await run(unity_catalog.list_volumes(client, catalog_name, schema_name, max_results, page_token))
|
|
587
|
+
|
|
588
|
+
# ---------------------------------------------------------------- commands (Command Execution 1.2)
|
|
589
|
+
|
|
590
|
+
@tool("commands", read_only=False, destructive=True)
|
|
591
|
+
async def run_command(
|
|
592
|
+
cluster_id: str,
|
|
593
|
+
command: str,
|
|
594
|
+
language: Language = "python",
|
|
595
|
+
timeout_seconds: Annotated[int, Field(ge=1, le=900)] = 120,
|
|
596
|
+
) -> str:
|
|
597
|
+
"""Run one snippet of code on a running cluster and return its result. Creates and
|
|
598
|
+
destroys a throwaway context; use create_execution_context to keep state between calls."""
|
|
599
|
+
return await run(command_execution.run_once(client, cluster_id, command, language, timeout_seconds))
|
|
600
|
+
|
|
601
|
+
@tool("commands", read_only=False)
|
|
602
|
+
async def create_execution_context(cluster_id: str, language: Language = "python") -> str:
|
|
603
|
+
"""Create a stateful execution context on a running cluster. Returns its id."""
|
|
604
|
+
return await run(command_execution.create_context(client, cluster_id, language))
|
|
605
|
+
|
|
606
|
+
@tool("commands", read_only=False, destructive=True)
|
|
607
|
+
async def execute_command(
|
|
608
|
+
cluster_id: str,
|
|
609
|
+
context_id: str,
|
|
610
|
+
command: str,
|
|
611
|
+
language: Language = "python",
|
|
612
|
+
wait: Annotated[bool, Field(description="Wait for the result instead of returning the command id.")] = True,
|
|
613
|
+
timeout_seconds: Annotated[int, Field(ge=1, le=900)] = 120,
|
|
614
|
+
) -> str:
|
|
615
|
+
"""Run code in an existing context; variables persist between calls."""
|
|
616
|
+
|
|
617
|
+
async def call() -> Any:
|
|
618
|
+
started = await command_execution.execute(client, cluster_id, context_id, command, language)
|
|
619
|
+
if not wait:
|
|
620
|
+
return started
|
|
621
|
+
return await command_execution.wait_for_command(
|
|
622
|
+
client, cluster_id, context_id, started["id"], timeout_seconds
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
return await run(call())
|
|
626
|
+
|
|
627
|
+
@tool("commands", read_only=True)
|
|
628
|
+
async def get_command_status(cluster_id: str, context_id: str, command_id: str) -> str:
|
|
629
|
+
"""Get a command's status and, when finished, its results."""
|
|
630
|
+
return await run(command_execution.command_status(client, cluster_id, context_id, command_id))
|
|
631
|
+
|
|
632
|
+
@tool("commands", read_only=False, idempotent=True)
|
|
633
|
+
async def cancel_command(cluster_id: str, context_id: str, command_id: str) -> str:
|
|
634
|
+
"""Cancel a running command."""
|
|
635
|
+
return await run(command_execution.cancel(client, cluster_id, context_id, command_id))
|
|
636
|
+
|
|
637
|
+
@tool("commands", read_only=False, idempotent=True)
|
|
638
|
+
async def destroy_execution_context(cluster_id: str, context_id: str) -> str:
|
|
639
|
+
"""Destroy an execution context and free its resources on the cluster."""
|
|
640
|
+
return await run(command_execution.destroy_context(client, cluster_id, context_id))
|
|
641
|
+
|
|
642
|
+
return server
|