|
| 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