Skip to content

Commit 13de3f0

Browse files
joaodaherclaude
andcommitted
feat(onboarding): reshape to get/validate/apply tools
Replace init/state with three purpose-built tools: - dbt_admin_onboarding_get: GET /onboarding/ — check current status - dbt_admin_onboarding_validate: POST /onboarding/validate/ — check data without applying - dbt_admin_onboarding_apply: POST /onboarding/ — submit data incrementally (idempotent) All three verified against the warm PR env. validate returns a clean error until the backend implements the /validate/ endpoint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7134bc0 commit 13de3f0

7 files changed

Lines changed: 155 additions & 109 deletions

File tree

src/dbt_mcp/dbt_admin/onboarding/client.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ async def _headers(self, config: AdminApiConfig) -> dict[str, str]:
3131
} | config.headers_provider.get_headers()
3232

3333
async def get(self, account_id: int) -> dict[str, Any] | None:
34-
"""Fetch the current onboarding model for the account.
34+
"""Fetch the current onboarding record for the account.
3535
36-
Returns the raw data dict, or None if no onboarding exists yet.
36+
Returns the raw data dict, or None if no onboarding exists yet (404).
3737
"""
3838
config = await self.config_provider.get_config()
3939
url = self._base_url(config, account_id)
@@ -49,41 +49,72 @@ async def get(self, account_id: int) -> dict[str, Any] | None:
4949
except httpx.HTTPStatusError as e:
5050
if 400 <= e.response.status_code < 500:
5151
raise InvalidParameterError(
52-
f"Onboarding request failed ({e.response.status_code}) for account {account_id}"
52+
f"Onboarding get failed ({e.response.status_code}) for account {account_id}"
5353
) from e
5454
raise AdminAPIError(
55-
f"Onboarding request failed ({e.response.status_code}) for account {account_id}"
55+
f"Onboarding get failed ({e.response.status_code}) for account {account_id}"
5656
) from e
5757
except httpx.HTTPError as e:
5858
raise AdminAPIError(
59-
f"Onboarding request failed for account {account_id}"
59+
f"Onboarding get failed for account {account_id}"
6060
) from e
6161

62-
async def create_or_get(self, account_id: int) -> dict[str, Any]:
63-
"""Create the onboarding model if it doesn't exist, or return the existing one.
62+
async def validate(self, account_id: int, data: dict[str, Any]) -> dict[str, Any]:
63+
"""Validate onboarding data without applying it.
6464
65-
POST is idempotent on the backend — returns the existing record if one is present.
65+
Returns a dict with 'valid' bool and 'errors' list.
6666
"""
6767
config = await self.config_provider.get_config()
68+
url = self._base_url(config, account_id) + "validate/"
69+
headers = await self._headers(config)
70+
71+
try:
72+
async with httpx.AsyncClient() as client:
73+
response = await client.post(
74+
url, headers=headers, json=data, follow_redirects=True
75+
)
76+
response.raise_for_status()
77+
return response.json()
78+
except httpx.HTTPStatusError as e:
79+
if 400 <= e.response.status_code < 500:
80+
# Validation errors are expected — return them as structured data
81+
try:
82+
return response.json()
83+
except Exception:
84+
pass
85+
raise InvalidParameterError(
86+
f"Onboarding validate failed ({e.response.status_code}) for account {account_id}"
87+
) from e
88+
raise AdminAPIError(
89+
f"Onboarding validate failed ({e.response.status_code}) for account {account_id}"
90+
) from e
91+
except httpx.HTTPError as e:
92+
raise AdminAPIError(
93+
f"Onboarding validate failed for account {account_id}"
94+
) from e
95+
96+
async def apply(self, account_id: int, data: dict[str, Any]) -> dict[str, Any]:
97+
"""Submit onboarding data. Idempotent — safe to call multiple times as data is gathered."""
98+
config = await self.config_provider.get_config()
6899
url = self._base_url(config, account_id)
69100
headers = await self._headers(config)
70101

71102
try:
72103
async with httpx.AsyncClient() as client:
73104
response = await client.post(
74-
url, headers=headers, json={}, follow_redirects=True
105+
url, headers=headers, json=data, follow_redirects=True
75106
)
76107
response.raise_for_status()
77108
return response.json().get("data", {})
78109
except httpx.HTTPStatusError as e:
79110
if 400 <= e.response.status_code < 500:
80111
raise InvalidParameterError(
81-
f"Onboarding create failed ({e.response.status_code}) for account {account_id}"
112+
f"Onboarding apply failed ({e.response.status_code}) for account {account_id}"
82113
) from e
83114
raise AdminAPIError(
84-
f"Onboarding create failed ({e.response.status_code}) for account {account_id}"
115+
f"Onboarding apply failed ({e.response.status_code}) for account {account_id}"
85116
) from e
86117
except httpx.HTTPError as e:
87118
raise AdminAPIError(
88-
f"Onboarding create failed for account {account_id}"
119+
f"Onboarding apply failed for account {account_id}"
89120
) from e

src/dbt_mcp/dbt_admin/onboarding/models.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,15 @@ def from_api(cls, data: dict[str, Any]) -> "OnboardingModel":
3535
)
3636

3737

38-
class OnboardingInitResult(BaseModel):
39-
onboarding: OnboardingModel
40-
created: bool
38+
class OnboardingGetResult(BaseModel):
39+
onboarding: OnboardingModel | None
4140

4241

43-
class OnboardingStateResult(BaseModel):
44-
onboarding: OnboardingModel | None
45-
decision_points: list[dict[str, Any]] = Field(default_factory=list)
42+
class OnboardingValidateResult(BaseModel):
43+
valid: bool
44+
errors: list[str] = Field(default_factory=list)
45+
missing_fields: list[str] = Field(default_factory=list)
46+
47+
48+
class OnboardingApplyResult(BaseModel):
49+
onboarding: OnboardingModel

src/dbt_mcp/dbt_admin/onboarding/tools.py

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from dataclasses import dataclass
2+
from typing import Any
23

34
from mcp.server.fastmcp import FastMCP
45

56
from dbt_mcp.config.config_providers import AdminApiConfig, ConfigProvider
67
from dbt_mcp.dbt_admin.onboarding.client import OnboardingClient
78
from dbt_mcp.dbt_admin.onboarding.models import (
8-
OnboardingInitResult,
9+
OnboardingApplyResult,
10+
OnboardingGetResult,
911
OnboardingModel,
10-
OnboardingStateResult,
12+
OnboardingValidateResult,
1113
)
1214
from dbt_mcp.prompts.prompts import get_prompt
1315
from dbt_mcp.tools.definitions import dbt_mcp_tool
@@ -23,57 +25,66 @@ class OnboardingToolContext:
2325

2426

2527
@dbt_mcp_tool(
26-
description=get_prompt("admin_api/onboarding_init"),
27-
title="Start or Resume Onboarding",
28-
read_only_hint=False,
28+
description=get_prompt("admin_api/onboarding_get"),
29+
title="Get Onboarding Status",
30+
read_only_hint=True,
2931
destructive_hint=False,
3032
idempotent_hint=True,
3133
)
32-
async def dbt_admin_onboarding_init(
34+
async def dbt_admin_onboarding_get(
3335
context: OnboardingToolContext,
34-
) -> OnboardingInitResult:
35-
"""Start or resume an onboarding session for the account."""
36+
) -> OnboardingGetResult:
37+
"""Return the current onboarding record, or null if none has been started."""
3638
config = await context.admin_api_config_provider.get_config()
37-
account_id = config.account_id
38-
39-
existing = await context.onboarding_client.get(account_id)
40-
if existing is not None:
41-
return OnboardingInitResult(
42-
onboarding=OnboardingModel.from_api(existing),
43-
created=False,
44-
)
45-
46-
data = await context.onboarding_client.create_or_get(account_id)
47-
return OnboardingInitResult(
48-
onboarding=OnboardingModel.from_api(data),
49-
created=True,
39+
data = await context.onboarding_client.get(config.account_id)
40+
return OnboardingGetResult(
41+
onboarding=OnboardingModel.from_api(data) if data is not None else None
5042
)
5143

5244

5345
@dbt_mcp_tool(
54-
description=get_prompt("admin_api/onboarding_state"),
55-
title="Get Onboarding State",
46+
description=get_prompt("admin_api/onboarding_validate"),
47+
title="Validate Onboarding Data",
5648
read_only_hint=True,
5749
destructive_hint=False,
5850
idempotent_hint=True,
5951
)
60-
async def dbt_admin_onboarding_state(
52+
async def dbt_admin_onboarding_validate(
6153
context: OnboardingToolContext,
62-
) -> OnboardingStateResult:
63-
"""Return the current onboarding state for the account."""
54+
data: dict[str, Any],
55+
) -> OnboardingValidateResult:
56+
"""Validate onboarding data without applying it; returns what is missing or invalid."""
6457
config = await context.admin_api_config_provider.get_config()
65-
account_id = config.account_id
58+
raw = await context.onboarding_client.validate(config.account_id, data)
59+
status = raw.get("status", {})
60+
if status.get("is_success"):
61+
return OnboardingValidateResult(valid=True)
62+
developer_message = status.get("developer_message", "")
63+
errors = [developer_message] if developer_message else []
64+
return OnboardingValidateResult(valid=False, errors=errors)
6665

67-
data = await context.onboarding_client.get(account_id)
68-
if data is None:
69-
return OnboardingStateResult(onboarding=None)
7066

71-
return OnboardingStateResult(onboarding=OnboardingModel.from_api(data))
67+
@dbt_mcp_tool(
68+
description=get_prompt("admin_api/onboarding_apply"),
69+
title="Apply Onboarding Data",
70+
read_only_hint=False,
71+
destructive_hint=False,
72+
idempotent_hint=True,
73+
)
74+
async def dbt_admin_onboarding_apply(
75+
context: OnboardingToolContext,
76+
data: dict[str, Any],
77+
) -> OnboardingApplyResult:
78+
"""Submit collected onboarding data; safe to call multiple times with partial data."""
79+
config = await context.admin_api_config_provider.get_config()
80+
raw = await context.onboarding_client.apply(config.account_id, data)
81+
return OnboardingApplyResult(onboarding=OnboardingModel.from_api(raw))
7282

7383

7484
ONBOARDING_TOOLS = [
75-
dbt_admin_onboarding_init,
76-
dbt_admin_onboarding_state,
85+
dbt_admin_onboarding_get,
86+
dbt_admin_onboarding_validate,
87+
dbt_admin_onboarding_apply,
7788
]
7889

7990

src/dbt_mcp/tools/readme_mappings.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@
6060
ToolName.LIST_JOB_RUN_ARTIFACTS: "Lists available artifacts from a job run.",
6161
ToolName.GET_JOB_RUN_ERROR: "Gets error and/or warning details for a job run; option to include or show warnings only.",
6262
# Onboarding tools
63-
ToolName.ONBOARDING_INIT: "Starts or resumes an onboarding session; returns session ID, current phase, and decision points.",
64-
ToolName.ONBOARDING_STATE: "Returns the current onboarding session phase and server-side applied resource progress.",
63+
ToolName.ONBOARDING_GET: "Returns the current onboarding record and progress; null if no onboarding has been started.",
64+
ToolName.ONBOARDING_VALIDATE: "Validates the collected onboarding data and returns what is missing or invalid.",
65+
ToolName.ONBOARDING_APPLY: "Submits collected onboarding data to the platform; call incrementally as each piece of data is gathered.",
6566
# dbt-codegen tools
6667
ToolName.GENERATE_SOURCE: "Generates source YAML by introspecting database schemas; option to include columns.",
6768
ToolName.GENERATE_MODEL_YAML: "Generates model YAML with columns; option to inherit upstream descriptions.",

src/dbt_mcp/tools/tool_names.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ class ToolName(Enum):
6565
GET_JOB_RUN_ERROR = "get_job_run_error"
6666

6767
# Onboarding tools
68-
ONBOARDING_INIT = "dbt_admin_onboarding_init"
69-
ONBOARDING_STATE = "dbt_admin_onboarding_state"
68+
ONBOARDING_GET = "dbt_admin_onboarding_get"
69+
ONBOARDING_VALIDATE = "dbt_admin_onboarding_validate"
70+
ONBOARDING_APPLY = "dbt_admin_onboarding_apply"
7071

7172
# dbt-codegen tools
7273
GENERATE_SOURCE = "generate_source"

src/dbt_mcp/tools/toolsets.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ class Toolset(Enum):
103103
ToolName.RETRY_JOB_RUN,
104104
ToolName.LIST_JOB_RUN_ARTIFACTS,
105105
ToolName.GET_JOB_RUN_ERROR,
106-
ToolName.ONBOARDING_INIT,
107-
ToolName.ONBOARDING_STATE,
106+
ToolName.ONBOARDING_GET,
107+
ToolName.ONBOARDING_VALIDATE,
108+
ToolName.ONBOARDING_APPLY,
108109
},
109110
Toolset.DBT_CODEGEN: {
110111
ToolName.GENERATE_SOURCE,

0 commit comments

Comments
 (0)