Skip to content

Commit bcc5d9e

Browse files
jgiuffridaclaude
andcommitted
Add meta field passthrough to tool definition infra for MCP Apps support
Adds the `meta` parameter to the tool definition pipeline so tools can declare associated UI resources via `meta={"ui": {"resourceUri": "ui://..."}}`. This is the plumbing needed to support MCP Apps without adding any features. Also adds CLAUDE.md, tool authoring docs in CONTRIBUTING.md, and tests for the meta passthrough. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e4d1177 commit bcc5d9e

6 files changed

Lines changed: 254 additions & 26 deletions

File tree

CLAUDE.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# CLAUDE.md
2+
3+
## Project Overview
4+
5+
dbt-mcp is an MCP (Model Context Protocol) server that exposes dbt functionality as tools to AI assistants. Built on `FastMCP` from the `mcp` SDK.
6+
7+
## Key Paths
8+
9+
- Entry point: `src/dbt_mcp/main.py`
10+
- Server: `src/dbt_mcp/mcp/server.py` (`DbtMCP` class, `create_dbt_mcp()`)
11+
- Tool infra: `src/dbt_mcp/tools/` (definitions, registration, injection, toolsets, tool_names)
12+
- Tool categories: `discovery/`, `semantic_layer/`, `dbt_cli/`, `dbt_codegen/`, `dbt_admin/`, `lsp/`, `mcp_server_metadata/`
13+
- Prompts (tool descriptions): `src/dbt_mcp/prompts/`
14+
- Config: `src/dbt_mcp/config/`
15+
- Tests: `tests/unit/`, `tests/integration/`
16+
17+
## Tool Architecture
18+
19+
Tools follow a consistent pattern:
20+
1. `@dbt_mcp_tool` decorator defines the tool with metadata
21+
2. `ToolName` enum in `tools/tool_names.py` — every tool needs an entry
22+
3. Toolset mapping in `tools/toolsets.py` — maps tools to categories
23+
4. Context injection via `adapt_context()` — tools receive typed context objects, but MCP only sees user-facing params
24+
5. `register_tools()` in `tools/register.py` — precedence-based enablement (individual > toolset > default)
25+
26+
### MCP Apps
27+
28+
Tools can have associated UIs via the `meta` field:
29+
- `meta={"ui": {"resourceUri": "ui://dbt-mcp/app-name"}}` on `@dbt_mcp_tool`
30+
- `structured_output=True` required — return type must be `TypedDict`
31+
- Register matching resource with `@dbt_mcp.resource(uri=..., mime_type="text/html;profile=mcp-app")`
32+
- Frontend uses `@modelcontextprotocol/ext-apps` SDK
33+
34+
## Commands
35+
36+
- `task test:unit` — run unit tests
37+
- `task test:integration` — run integration tests (requires dbt Platform credentials)
38+
- `task install` — install dependencies
39+
- `task dev` — run server with streamable-http transport
40+
- `task inspector` — run with MCP Inspector
41+
- `uv run pytest tests/ --ignore=tests/integration -x -q` — quick unit test run
42+
43+
## Style
44+
45+
- See `.cursor/rules/python.mdc` for Python conventions
46+
- Import at top of file, type annotations on all functions
47+
- Prefer Pydantic models or dataclasses over dicts
48+
- Use `*` in param lists when adjacent params share a type
49+
- Avoid code in `__init__.py`
50+
51+
## Testing
52+
53+
- `MockFastMCP` in `tests/conftest.py` captures registered tools and their kwargs (including `meta`)
54+
- Tool definition tests: `tests/unit/tools/test_definitions.py`
55+
- Precedence logic tests: `tests/unit/tools/test_precedence.py`

CONTRIBUTING.md

Lines changed: 92 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ With [task](https://taskfile.dev/) installed, simply run `task` to see the list
55
## Setup
66

77
1. Clone the repository:
8+
89
```shell
910
git clone https://github.com/dbt-labs/dbt-mcp.git
1011
cd dbt-mcp
@@ -17,10 +18,12 @@ cd dbt-mcp
1718
4. Run `task install`
1819

1920
5. Configure environment variables:
20-
```shell
21-
cp .env.example .env
22-
```
23-
Then edit `.env` with your specific environment variables (see [our docs](https://docs.getdbt.com/docs/dbt-ai/setup-local-mcp) to determine the values).
21+
22+
```shell
23+
cp .env.example .env
24+
```
25+
26+
Then edit `.env` with your specific environment variables (see [our docs](https://docs.getdbt.com/docs/dbt-ai/setup-local-mcp) to determine the values).
2427

2528
6. Run `task client` to chat with dbt MCP in your terminal.
2629

@@ -85,6 +88,63 @@ Or, if you would like to test with Oauth, use a configuration like this:
8588

8689
For improved debugging, you can set the `DBT_MCP_SERVER_FILE_LOGGING=true` environment variable to log to a `./dbt-mcp.log` file.
8790

91+
## Adding Tools
92+
93+
Tools are defined using the `@dbt_mcp_tool` decorator and registered with the MCP server via `register_tools()`.
94+
95+
### Steps to add a new tool
96+
97+
1. **Add a `ToolName` entry** in `src/dbt_mcp/tools/tool_names.py`
98+
2. **Map it to a toolset** in `src/dbt_mcp/tools/toolsets.py` (e.g., `DISCOVERY`, `SEMANTIC_LAYER`, `SQL`)
99+
3. **Write a prompt file** in `src/dbt_mcp/prompts/<category>/` describing the tool
100+
4. **Define the tool function** in the appropriate `tools.py` module:
101+
```python
102+
@dbt_mcp_tool(
103+
description=get_prompt("category/tool_name"),
104+
title="My Tool",
105+
read_only_hint=True,
106+
destructive_hint=False,
107+
idempotent_hint=True,
108+
)
109+
async def my_tool(context: MyToolContext, param: str) -> dict:
110+
return await context.fetcher.do_something(param)
111+
```
112+
5. **Add it to the tool list** (e.g., `DISCOVERY_TOOLS`) in the same module
113+
6. The registration function (e.g., `register_discovery_tools`) handles context binding and registration
114+
115+
### Adding an MCP App tool
116+
117+
MCP Apps are tools that have an associated interactive UI rendered by the host. They build on top of regular tools with two additions:
118+
119+
1. **Use `structured_output=True` and `meta`** to link the tool to a UI resource:
120+
```python
121+
@dbt_mcp_tool(
122+
description=get_prompt("category/tool_name"),
123+
title="My Visualization",
124+
read_only_hint=True,
125+
structured_output=True,
126+
meta={"ui": {"resourceUri": "ui://dbt-mcp/my-app"}},
127+
)
128+
async def my_viz_tool(context: MyToolContext, param: str) -> MyResultType:
129+
...
130+
```
131+
The return type must be a `TypedDict` when `structured_output=True`.
132+
133+
2. **Register an MCP resource** at the matching `ui://` URI to serve the HTML app:
134+
```python
135+
@dbt_mcp.resource(
136+
uri="ui://dbt-mcp/my-app",
137+
name="My App",
138+
mime_type="text/html;profile=mcp-app",
139+
)
140+
def get_my_app_ui() -> str:
141+
return Path("packages/my-app/dist/index.html").read_text()
142+
```
143+
144+
3. **Build a frontend** in `packages/` using `@modelcontextprotocol/ext-apps`. The app receives tool results via the `ontoolresult` callback and must be bundled as a single HTML file (e.g., using `vite-plugin-singlefile`) or reference external resources via CSP `resourceDomains`.
145+
146+
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`).
147+
88148
## Signed Commits
89149

90150
Before committing changes, ensure that you have set up [signed commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
@@ -99,19 +159,22 @@ Every PR requires a changelog entry. [Install changie](https://changie.dev/) and
99159
The dbt-mcp server runs with `stdio` transport by default which does not allow for Python debugger support. For debugging with breakpoints, use `streamable-http` transport.
100160

101161
### Option 1: MCP Inspector Only (No Breakpoints)
162+
102163
1. Run `task inspector` - this starts both the server and inspector automatically
103164
2. Open MCP Inspector UI
104165
3. Use "STDIO" Transport Type to connect
105166
4. Test tools interactively in the inspector UI (uses `stdio` transport, no debugger support)
106167

107168
### Option 2: VS Code Debugger with Breakpoints (Recommended for Debugging)
169+
108170
1. Set breakpoints in your code
109171
2. Press `F5` or select "debug dbt-mcp" from the Run menu
110172
3. Open MCP Inspector UI via `npx @modelcontextprotocol/inspector`
111173
4. Connect to `http://localhost:8000/mcp/v1` using "Streamable HTTP" transport and "Via Proxy" connection type
112174
5. Call tools from Inspector - your breakpoints will trigger
113175

114176
### Option 3: Manual Debugging with `task dev`
177+
115178
1. Run `task dev` - this starts the server with `streamable-http` transport on `http://localhost:8000`
116179
2. Set breakpoints in your code
117180
3. Attach your debugger manually (see [debugpy documentation](https://github.com/microsoft/debugpy#debugpy) for examples)
@@ -128,29 +191,32 @@ If you encounter any problems, you can try running `task run` to see errors in y
128191
Only people in the `CODEOWNERS` file should trigger a new release with these steps:
129192

130193
1. Consider these guidelines when choosing a version number:
131-
- Major
132-
- Removing a tool or toolset without replacement, or in a way that would result in agents behaving much differently.
133-
- Changing the behavior of existing environment variables or configurations
134-
- Minor
135-
- Changes to config system related to the function signature of the register functions (e.g. `register_discovery_tools`)
136-
- Adding optional parameters to a tool function signature
137-
- Adding a new tool or toolset
138-
- Removing or adding non-optional parameters from tool function signatures
139-
- Renaming a tool
140-
- Removing a tool which overlaps or provides similar functionality as other tool(s)
141-
- Patch
142-
- Bug and security fixes - only major security and bug fixes will be back-ported to prior minor and major versions
143-
- Dependency updates which don’t change behavior
144-
- Minor enhancements
145-
- Editing a tool or parameter description prompt
146-
- Adding an allowed environment variable with the `DBT_MCP_` prefix
194+
195+
- Major
196+
- Removing a tool or toolset without replacement, or in a way that would result in agents behaving much differently.
197+
- Changing the behavior of existing environment variables or configurations
198+
- Minor
199+
- Changes to config system related to the function signature of the register functions (e.g. `register_discovery_tools`)
200+
- Adding optional parameters to a tool function signature
201+
- Adding a new tool or toolset
202+
- Removing or adding non-optional parameters from tool function signatures
203+
- Renaming a tool
204+
- Removing a tool which overlaps or provides similar functionality as other tool(s)
205+
- Patch
206+
- Bug and security fixes - only major security and bug fixes will be back-ported to prior minor and major versions
207+
- Dependency updates which don’t change behavior
208+
- Minor enhancements
209+
- Editing a tool or parameter description prompt
210+
- Adding an allowed environment variable with the `DBT_MCP_` prefix
211+
147212
2. Trigger the [Create release PR Action](https://github.com/dbt-labs/dbt-mcp/actions/workflows/create-release-pr.yml).
148-
- If the release is NOT a pre-release, select `auto` (default) to automatically determine the version bump based on changelog entries, or manually pick patch, minor or major if needed
149-
- If the release is a pre-release, set the bump and the pre-release suffix. We support alpha.N, beta.N and rc.N.
150-
- use alpha for early releases of experimental features that specific people might want to test. Significant changes can be expected between alpha and the official release.
151-
- use beta for releases that are mostly stable but still in development. It can be used to gather feedback from a group of peopleon how a specific feature should work.
152-
- use rc for releases that are mostly stable and already feature complete. Only bugfixes and minor changes are expected between rc and the official release.
153-
- Picking the prerelease suffix will depend on whether the last release was the stable release or a pre-release:
213+
214+
- If the release is NOT a pre-release, select `auto` (default) to automatically determine the version bump based on changelog entries, or manually pick patch, minor or major if needed
215+
- If the release is a pre-release, set the bump and the pre-release suffix. We support alpha.N, beta.N and rc.N.
216+
- use alpha for early releases of experimental features that specific people might want to test. Significant changes can be expected between alpha and the official release.
217+
- use beta for releases that are mostly stable but still in development. It can be used to gather feedback from a group of peopleon how a specific feature should work.
218+
- use rc for releases that are mostly stable and already feature complete. Only bugfixes and minor changes are expected between rc and the official release.
219+
- Picking the prerelease suffix will depend on whether the last release was the stable release or a pre-release:
154220

155221
| Last Stable | Last Pre-release | Bump | Pre-release Suffix | Resulting Version |
156222
| ----------- | ---------------- | ----- | ------------------ | ----------------- |

src/dbt_mcp/tools/definitions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class GenericToolDefinition[NameEnum: Enum]:
2222
# We haven't strictly defined our tool contracts yet.
2323
# So we're setting this to False by default for now.
2424
structured_output: bool | None = False
25+
meta: dict[str, Any] | None = None
2526

2627
def get_name(self) -> NameEnum:
2728
return self.name_enum((self.name or self.fn.__name__).lower())
@@ -34,6 +35,7 @@ def to_fastmcp_internal_tool(self) -> Tool:
3435
description=self.description,
3536
annotations=self.annotations,
3637
structured_output=self.structured_output,
38+
meta=self.meta,
3739
)
3840

3941
def adapt_context(
@@ -50,6 +52,7 @@ def adapt_context(
5052
title=self.title,
5153
annotations=self.annotations,
5254
structured_output=self.structured_output,
55+
meta=self.meta,
5356
)
5457

5558

@@ -68,6 +71,7 @@ def generic_dbt_mcp_tool[NameEnum: Enum](
6871
idempotent_hint: bool = False,
6972
open_world_hint: bool = True,
7073
structured_output: bool | None = False,
74+
meta: dict[str, Any] | None = None,
7175
) -> Callable[[Callable], GenericToolDefinition[NameEnum]]:
7276
"""Decorator to define a tool definition for dbt MCP"""
7377

@@ -86,6 +90,7 @@ def decorator(fn: Callable) -> GenericToolDefinition[NameEnum]:
8690
openWorldHint=open_world_hint,
8791
),
8892
structured_output=structured_output,
93+
meta=meta,
8994
)
9095

9196
return decorator

src/dbt_mcp/tools/register.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,5 @@ def generic_register_tools[NameEnum: Enum](
123123
description=tool_definition.description,
124124
annotations=tool_definition.annotations,
125125
structured_output=tool_definition.structured_output,
126+
meta=tool_definition.meta,
126127
)

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,11 @@ def path(relpath: str):
125125
class MockFastMCP:
126126
def __init__(self):
127127
self.tools = {}
128+
self.tool_kwargs = {}
128129

129130
def add_tool(self, fn: Callable[..., Any], **kwargs):
130131
self.tools[fn.__name__] = fn
132+
self.tool_kwargs[fn.__name__] = kwargs
131133

132134
def tool(self, **kwargs):
133135
def decorator(func):
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Unit tests for tool definition infrastructure."""
2+
3+
from enum import Enum
4+
from typing import Any
5+
6+
from dbt_mcp.tools.definitions import GenericToolDefinition, generic_dbt_mcp_tool
7+
from dbt_mcp.tools.register import generic_register_tools
8+
from dbt_mcp.tools.toolsets import Toolset
9+
10+
11+
class FakeToolName(Enum):
12+
MY_TOOL = "my_tool"
13+
14+
15+
def _make_tool(
16+
meta: dict[str, Any] | None = None,
17+
) -> GenericToolDefinition[FakeToolName]:
18+
"""Helper to create a tool definition with optional meta."""
19+
20+
@generic_dbt_mcp_tool(
21+
description="test tool",
22+
name_enum=FakeToolName,
23+
title="Test Tool",
24+
read_only_hint=True,
25+
meta=meta,
26+
)
27+
async def my_tool() -> str:
28+
return "ok"
29+
30+
return my_tool
31+
32+
33+
class TestMetaPassthrough:
34+
"""Test that the meta field is preserved through all operations."""
35+
36+
def test_decorator_sets_meta(self):
37+
meta = {"ui": {"resourceUri": "ui://test/app.html"}}
38+
tool = _make_tool(meta=meta)
39+
assert tool.meta == meta
40+
41+
def test_decorator_meta_defaults_to_none(self):
42+
tool = _make_tool()
43+
assert tool.meta is None
44+
45+
def test_adapt_context_preserves_meta(self):
46+
meta = {"ui": {"resourceUri": "ui://test/app.html"}}
47+
tool = _make_tool(meta=meta)
48+
49+
def mapper() -> None:
50+
return None
51+
52+
adapted = tool.adapt_context(mapper)
53+
assert adapted.meta == meta
54+
55+
def test_to_fastmcp_internal_tool_passes_meta(self):
56+
meta = {"ui": {"resourceUri": "ui://test/app.html"}}
57+
tool = _make_tool(meta=meta)
58+
59+
internal = tool.to_fastmcp_internal_tool()
60+
assert internal.meta == meta
61+
62+
def test_to_fastmcp_internal_tool_none_meta(self):
63+
tool = _make_tool()
64+
65+
internal = tool.to_fastmcp_internal_tool()
66+
assert internal.meta is None
67+
68+
def test_register_tools_passes_meta(self, mock_fastmcp):
69+
mock_mcp, _ = mock_fastmcp
70+
meta = {"ui": {"resourceUri": "ui://test/app.html"}}
71+
tool = _make_tool(meta=meta)
72+
73+
generic_register_tools(
74+
mock_mcp,
75+
[tool],
76+
disabled_tools=set(),
77+
enabled_tools=None,
78+
enabled_toolsets=set(),
79+
disabled_toolsets=set(),
80+
tool_to_toolset={FakeToolName.MY_TOOL: Toolset.DISCOVERY},
81+
)
82+
83+
assert mock_mcp.tool_kwargs["my_tool"]["meta"] == meta
84+
85+
def test_register_tools_passes_none_meta(self, mock_fastmcp):
86+
mock_mcp, _ = mock_fastmcp
87+
tool = _make_tool()
88+
89+
generic_register_tools(
90+
mock_mcp,
91+
[tool],
92+
disabled_tools=set(),
93+
enabled_tools=None,
94+
enabled_toolsets=set(),
95+
disabled_toolsets=set(),
96+
tool_to_toolset={FakeToolName.MY_TOOL: Toolset.DISCOVERY},
97+
)
98+
99+
assert mock_mcp.tool_kwargs["my_tool"]["meta"] is None

0 commit comments

Comments
 (0)