Skip to content

Commit 0739d2c

Browse files
docs: add deprecate-then-remove process guide for MCP tools (#826)
# Why Tools in dbt-mcp can't be removed immediately — the server is a published app in the Anthropic and OpenAI app stores, and removing a tool is a breaking change for all installed clients. Without a written process, it's easy to accidentally delete a tool outright. This adds a canonical reference so every future deprecation follows the same pattern. # What - **`docs/deprecating-tools.md`** — new standalone process guide covering: - Why direct removal breaks published clients (links to the existing `Published-app contract` section and `contract/snapshot.py`) - The 4-step deprecate-then-remove lifecycle - Mechanical deprecation checklist using the helpers in `src/dbt_mcp/tools/deprecation.py` (`deprecated_description` + `deprecation_meta`), with `get_model_parents` as a worked example from the stacked PR - Why the deprecated description should be short and blunt rather than the original prompt with a banner prepended — it makes a model less likely to pick the tool, which speeds the usage soak — plus an optional note on forwarding a deprecated tool to its replacement internally when the replacement is a true drop-in - What NOT to delete (ToolName, toolsets, readme_mappings, `*_TOOLS` lists, prompt file, client code) - A **Release & rollout** section: merging a dbt-mcp PR doesn't reach the published apps on its own — it has to be released, rolled into the hosted MCP service, and resubmitted to each app store afterward - The `ToolCalled` telemetry signal and the ~0 usage bar for safe removal, measured against the deployed service rather than the merged PR - Phase B removal checklist - **`CONTRIBUTING.md`** — a `## Deprecating a tool` section (after `## Adding Tools`) that summarizes the lifecycle in two sentences and links to the new doc Drafted by Claude Sonnet 4.6 under the direction of @theyostalservice --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d6082e5 commit 0739d2c

3 files changed

Lines changed: 169 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Under the Hood
2+
body: 'Add docs/deprecating-tools.md: full process guide for the deprecate-then-remove tool lifecycle, plus CONTRIBUTING.md pointer.'
3+
time: 2026-06-29T13:31:55.93604-07:00

CONTRIBUTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,14 @@ MCP Apps are tools that have an associated interactive UI rendered by the host (
172172

173173
The `ui://` URI convention is `ui://<server-name>/<resource-name>`. The `meta` field is passed through the full tool registration pipeline (`@dbt_mcp_tool``GenericToolDefinition``adapt_context``register_tools``FastMCP.add_tool`).
174174

175+
## Deprecating a tool
176+
177+
Tools in dbt-mcp follow a **deprecate-then-remove** lifecycle — because the server is
178+
a published app, you cannot remove a tool immediately without breaking installed clients.
179+
See [`docs/deprecating-tools.md`](docs/deprecating-tools.md) for the full process,
180+
including the 4-step lifecycle, the mechanical deprecation checklist, and instructions
181+
for monitoring usage before removal.
182+
175183
## Published-app contract
176184

177185
When the server is published as an app (notably the ChatGPT app), the host caches the server's metadata as a versioned contract at submission time: tool names/titles/descriptions, input/output schemas, annotations, `_meta`, linked UI resource metadata, and the server `instructions`. Deploying a server change does **not** update that published snapshot, and breaking changes (removing/renaming a tool, making an input schema incompatible, or changing the content served at a published UI resource URI) can break the published version as soon as they deploy.

docs/deprecating-tools.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Deprecating a tool
2+
3+
Tools in dbt-mcp follow a **deprecate-then-remove** lifecycle rather than being
4+
deleted outright. This document explains why and how.
5+
6+
## Why tools can't just be deleted
7+
8+
The dbt MCP server is a **published app** in the Anthropic and OpenAI app stores.
9+
When an app is submitted, the host caches the server's tool surface — names, titles,
10+
descriptions, input/output schemas, annotations, and `_meta` — as a versioned
11+
contract at submission time. Removing or renaming a tool is a breaking change for
12+
all installed clients that depend on that tool string.
13+
14+
Deploying the server immediately removes a tool from the live server, but the
15+
published contract (the snapshot submitted to the app store) still lists it. Clients
16+
using the published version see a mismatch; the tool disappears without warning.
17+
See `CONTRIBUTING.md` (§ Published-app contract) and
18+
`src/dbt_mcp/contract/snapshot.py` for technical details.
19+
20+
## The deprecate-then-remove lifecycle
21+
22+
1. **Add the replacement tool.** Ship the new, consolidated tool. Its contract
23+
snapshot is submitted with the next app-store update.
24+
2. **Deprecate the old tool.** Keep it registered and callable. Apply a deprecation
25+
banner to its description and a `deprecated` / `replacement` signal to its `meta`.
26+
Publish a compatible app update.
27+
3. **Monitor usage.** Track calls to the deprecated tool name via the `ToolCalled`
28+
telemetry event (see [Monitoring](#monitoring-usage-before-removal)). Wait until
29+
usage falls to approximately zero over a trailing 30-day window.
30+
4. **Remove the tool.** Delete it in a separate PR, bump the major version, and
31+
submit a breaking-change app update.
32+
33+
## How to deprecate a tool
34+
35+
Use the helpers in `src/dbt_mcp/tools/deprecation.py`:
36+
37+
```python
38+
from dbt_mcp.tools.deprecation import deprecated_description, deprecation_meta
39+
40+
@dbt_mcp_tool(
41+
description=deprecated_description(replacement="get_lineage"),
42+
meta=deprecation_meta(replacement="get_lineage"),
43+
title="Get Model Parents",
44+
read_only_hint=True,
45+
destructive_hint=False,
46+
idempotent_hint=True,
47+
)
48+
async def get_model_parents(...):
49+
...
50+
```
51+
52+
`deprecated_description` **replaces** the tool's description entirely — it does
53+
not prepend to the original prompt. It builds a short, blunt line:
54+
55+
> **DEPRECATED — use \`get_lineage\` instead.** This tool will be removed in a
56+
> future release.
57+
58+
A shorter description makes a model less likely to pick the deprecated tool,
59+
which speeds the usage soak before removal. Pass `removal_version="vX.Y"` if a
60+
target removal version is already known, and `arg_mapping="..."` for a one-line
61+
note on how arguments map to the replacement — only needed when the replacement
62+
isn't a drop-in (see [What to change](#what-to-change) below).
63+
64+
`deprecation_meta` returns `{"deprecated": True, "replacement": "<name>"}`, which
65+
surfaces as `Tool._meta` in the MCP protocol so clients can inspect it
66+
programmatically.
67+
68+
### What to change
69+
70+
- Apply `deprecated_description(...)` + `deprecation_meta(replacement="<new>")` at
71+
the `@dbt_mcp_tool(...)` call site in **both** `discovery/tools.py` and
72+
`discovery/tools_multiproject.py`.
73+
- **Optional — forward to the replacement.** If the replacement is a true
74+
drop-in (same inputs, same output shape), you may reimplement the deprecated
75+
tool's body as a thin call into the replacement to cut maintenance. Only do
76+
this when it doesn't change behavior — keep the deprecated tool's own schema
77+
and output contract exactly as they were. If the replacement isn't a drop-in
78+
(different required arguments, a reshaped output), don't force it; document
79+
the mapping instead with `arg_mapping=` and leave the implementation as-is.
80+
81+
### What NOT to change
82+
83+
Do **not** delete anything during deprecation — the tool must stay registered and
84+
callable. Keep all of the following intact:
85+
86+
- `ToolName` enum entry in `tools/tool_names.py`
87+
- Toolset mapping in `tools/toolsets.py`
88+
- Human description in `tools/readme_mappings.py`
89+
- Entry in `DISCOVERY_TOOLS` / `MULTIPROJECT_DISCOVERY_TOOLS`
90+
- Prompt file in `prompts/discovery/`
91+
- Client fetch code (GraphQL query, fetcher method)
92+
93+
### Checklist
94+
95+
- [ ] Apply `deprecated_description` + `deprecation_meta` in both `tools.py` files
96+
- [ ] `changie new --kind "Enhancement or New Feature" --body "Deprecated <tool>; use <replacement> instead."`
97+
- [ ] `task docs:generate`
98+
- [ ] `task contract:generate` — regenerates `tests/unit/contract/contract_snapshot.json`; commit the result
99+
- [ ] `task check` + `task test:unit`
100+
- [ ] Release and roll out — merging this PR doesn't update the published apps
101+
on its own (see [Release & rollout](#release--rollout))
102+
103+
## Release & rollout
104+
105+
A merged dbt-mcp PR does **not** reach the published apps on its own — merging
106+
is not releasing, and releasing is not deploying. The same rollout applies to
107+
both the deprecate and the remove phases:
108+
109+
1. **Merge the dbt-mcp PR** (code + contract snapshot + changelog entry).
110+
2. **Cut a dbt-mcp release.** See `CONTRIBUTING.md` (§ Release). A deprecation
111+
is typically a minor bump; a removal is a major bump.
112+
3. **Roll the new release into the hosted MCP service and deploy it.** This is
113+
the step that actually changes the tool surface the published apps talk to.
114+
A major bump (a removal) requires updating the pinned dbt-mcp version in the
115+
hosted service, not just a routine dependency refresh.
116+
4. **Resubmit the app version in each store (OpenAI and Claude), after step 3.**
117+
Each store is a separate submission, and each captures the live server's
118+
tool list at submission time — submitting before the hosted service runs
119+
the new release captures the old surface.
120+
121+
## Monitoring usage before removal
122+
123+
Every tool call emits a `ToolCalled` telemetry event keyed on the field `tool_name`
124+
(the registered tool string, e.g. `"get_model_parents"`), with feature tag
125+
`dbt-mcp`. The event is emitted in `src/dbt_mcp/mcp/server.py` (`DbtMCP.call_tool`)
126+
and flows through `src/dbt_mcp/tracking/tracking.py` (`emit_tool_called_event`).
127+
Proxied tools are filtered out before emission — only tools directly served by this
128+
repo are tracked.
129+
130+
Query `ToolCalled` events filtered to the deprecated `tool_name` over a trailing
131+
30-day window. The bar for proceeding to removal is approximately zero calls —
132+
measured against the *deployed* hosted service, not just the merged PR (see
133+
[Release & rollout](#release--rollout)).
134+
135+
## Removing a tool (Phase B)
136+
137+
Once usage is ~0, remove the tool in a dedicated PR:
138+
139+
1. Delete the tool function from `discovery/tools.py` and `tools_multiproject.py`,
140+
and its entry in `DISCOVERY_TOOLS` / `MULTIPROJECT_DISCOVERY_TOOLS`.
141+
2. Delete from `tools/tool_names.py`, `tools/toolsets.py`,
142+
`tools/readme_mappings.py`.
143+
3. Delete the prompt file from `prompts/discovery/`.
144+
4. Delete the `deprecated_description` / `deprecation_meta` call sites added in the
145+
deprecation PR.
146+
5. Delete now-unused client code (grep-confirm no other callers first).
147+
6. Update the round-trip tests (`test_tool_names.py` / `test_toolsets.py`) to remove
148+
the tool from the expected set.
149+
7. `changie new --kind "Breaking Change" --body "Removed <tool>."`
150+
8. `task docs:generate`
151+
9. `task contract:generate` — commit the updated snapshot
152+
10. `task check` + `task test:unit`
153+
11. Release and roll out (see [Release & rollout](#release--rollout)).
154+
155+
The tool leaves the live server only once the hosted service deploys this
156+
removal, and the published contract still lists it until each store approves
157+
the new app version — so confirm usage is ~0 against the deployed service
158+
before that deploy, not merely before this PR merges.

0 commit comments

Comments
 (0)