-
Notifications
You must be signed in to change notification settings - Fork 122
Add meta field passthrough for MCP Apps support #624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
bcc5d9e
Add meta field passthrough to tool definition infra for MCP Apps support
jgiuffrida 5ae83d6
Add changelog entry for MCP Apps infra
jgiuffrida 99622e4
Address PR review feedback on docs
jgiuffrida d338fc3
Merge branch 'main' into feat/mcp-app-infra
jgiuffrida 7a2cfe4
Add task docs:generate to CLAUDE.md commands
jgiuffrida b71b90f
Add check and fmt commands to CLAUDE.md
jgiuffrida 1b5d574
Merge branch 'main' into feat/mcp-app-infra
jgiuffrida File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
3 changes: 3 additions & 0 deletions
3
.changes/unreleased/Under the Hood-20260304-mcp-app-infra.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| kind: Under the Hood | ||
| body: Add meta field passthrough to tool definition infra for MCP Apps support | ||
| time: 2026-03-04T10:00:00.000000-08:00 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # CLAUDE.md | ||
|
|
||
| ## Project Overview | ||
|
|
||
| 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. | ||
|
|
||
| ## Key Paths | ||
|
|
||
| - Entry point: `src/dbt_mcp/main.py` | ||
| - Server: `src/dbt_mcp/mcp/server.py` (`DbtMCP` class, `create_dbt_mcp()`) | ||
| - Tool infra: `src/dbt_mcp/tools/` (definitions, registration, injection, toolsets, tool_names) | ||
| - Tool categories: `discovery/`, `semantic_layer/`, `dbt_cli/`, `dbt_codegen/`, `dbt_admin/`, `lsp/`, `mcp_server_metadata/` | ||
| - Prompts (tool descriptions): `src/dbt_mcp/prompts/` | ||
| - Config: `src/dbt_mcp/config/` | ||
| - Tests: `tests/unit/`, `tests/integration/` | ||
|
|
||
| ## Tool Architecture | ||
|
|
||
| Tools follow a consistent pattern: | ||
| 1. `@dbt_mcp_tool` decorator defines the tool with metadata | ||
| 2. `ToolName` enum in `tools/tool_names.py` — every tool needs an entry | ||
| 3. Toolset mapping in `tools/toolsets.py` — maps tools to categories | ||
| 4. Context injection via `adapt_context()` — tools receive typed context objects, but MCP only sees user-facing params | ||
| 5. `register_tools()` in `tools/register.py` — precedence-based enablement (individual > toolset > default) | ||
|
|
||
| ### MCP Apps (tools with interactive UI) | ||
|
|
||
| Tools can have associated UIs via the `meta` field: | ||
| - `meta={"ui": {"resourceUri": "ui://dbt-mcp/app-name"}}` on `@dbt_mcp_tool` | ||
| - `structured_output=True` required so the host can pass structured JSON to the UI | ||
| - Return type should be a Pydantic model | ||
| - Register matching resource with `@dbt_mcp.resource(uri=..., mime_type="text/html;profile=mcp-app")` | ||
| - Frontend uses `@modelcontextprotocol/ext-apps` SDK | ||
|
|
||
| ## Commands | ||
|
|
||
| - `task test:unit` — run unit tests | ||
| - `task test:integration` — run integration tests (requires dbt Platform credentials) | ||
| - `task install` — install dependencies | ||
| - `task dev` — run server with streamable-http transport | ||
| - `task inspector` — run with MCP Inspector | ||
| - `uv run pytest tests/ --ignore=tests/integration -x -q` — quick unit test run | ||
|
|
||
| ## Style | ||
|
|
||
| - See `.cursor/rules/python.mdc` for Python conventions | ||
| - Import at top of file, type annotations on all functions | ||
| - Prefer Pydantic models or dataclasses over dicts | ||
| - Use `*` in param lists when adjacent params share a type | ||
| - Avoid code in `__init__.py` | ||
|
|
||
| ## Testing | ||
|
|
||
| - `MockFastMCP` in `tests/conftest.py` captures registered tools and their kwargs (including `meta`) | ||
| - Tool definition tests: `tests/unit/tools/test_definitions.py` | ||
| - Precedence logic tests: `tests/unit/tools/test_precedence.py` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ class GenericToolDefinition[NameEnum: Enum]: | |
| # We haven't strictly defined our tool contracts yet. | ||
| # So we're setting this to False by default for now. | ||
| structured_output: bool | None = False | ||
| meta: dict[str, Any] | None = None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't there a risk to confuse ourselves/people between the MCP tool |
||
|
|
||
| def get_name(self) -> NameEnum: | ||
| return self.name_enum((self.name or self.fn.__name__).lower()) | ||
|
|
@@ -34,6 +35,7 @@ def to_fastmcp_internal_tool(self) -> Tool: | |
| description=self.description, | ||
| annotations=self.annotations, | ||
| structured_output=self.structured_output, | ||
| meta=self.meta, | ||
| ) | ||
|
|
||
| def adapt_context( | ||
|
|
@@ -50,6 +52,7 @@ def adapt_context( | |
| title=self.title, | ||
| annotations=self.annotations, | ||
| structured_output=self.structured_output, | ||
| meta=self.meta, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -68,6 +71,7 @@ def generic_dbt_mcp_tool[NameEnum: Enum]( | |
| idempotent_hint: bool = False, | ||
| open_world_hint: bool = True, | ||
| structured_output: bool | None = False, | ||
| meta: dict[str, Any] | None = None, | ||
| ) -> Callable[[Callable], GenericToolDefinition[NameEnum]]: | ||
| """Decorator to define a tool definition for dbt MCP""" | ||
|
|
||
|
|
@@ -86,6 +90,7 @@ def decorator(fn: Callable) -> GenericToolDefinition[NameEnum]: | |
| openWorldHint=open_world_hint, | ||
| ), | ||
| structured_output=structured_output, | ||
| meta=meta, | ||
| ) | ||
|
|
||
| return decorator | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| """Unit tests for tool definition infrastructure.""" | ||
|
|
||
| from enum import Enum | ||
| from typing import Any | ||
|
|
||
| from dbt_mcp.tools.definitions import GenericToolDefinition, generic_dbt_mcp_tool | ||
| from dbt_mcp.tools.register import generic_register_tools | ||
| from dbt_mcp.tools.toolsets import Toolset | ||
|
|
||
|
|
||
| class FakeToolName(Enum): | ||
| MY_TOOL = "my_tool" | ||
|
|
||
|
|
||
| def _make_tool( | ||
| meta: dict[str, Any] | None = None, | ||
| ) -> GenericToolDefinition[FakeToolName]: | ||
| """Helper to create a tool definition with optional meta.""" | ||
|
|
||
| @generic_dbt_mcp_tool( | ||
| description="test tool", | ||
| name_enum=FakeToolName, | ||
| title="Test Tool", | ||
| read_only_hint=True, | ||
| meta=meta, | ||
| ) | ||
| async def my_tool() -> str: | ||
| return "ok" | ||
|
|
||
| return my_tool | ||
|
|
||
|
|
||
| class TestMetaPassthrough: | ||
| """Test that the meta field is preserved through all operations.""" | ||
|
|
||
| def test_decorator_sets_meta(self): | ||
| meta = {"ui": {"resourceUri": "ui://test/app.html"}} | ||
| tool = _make_tool(meta=meta) | ||
| assert tool.meta == meta | ||
|
|
||
| def test_decorator_meta_defaults_to_none(self): | ||
| tool = _make_tool() | ||
| assert tool.meta is None | ||
|
|
||
| def test_adapt_context_preserves_meta(self): | ||
| meta = {"ui": {"resourceUri": "ui://test/app.html"}} | ||
| tool = _make_tool(meta=meta) | ||
|
|
||
| def mapper() -> None: | ||
| return None | ||
|
|
||
| adapted = tool.adapt_context(mapper) | ||
| assert adapted.meta == meta | ||
|
|
||
| def test_to_fastmcp_internal_tool_passes_meta(self): | ||
| meta = {"ui": {"resourceUri": "ui://test/app.html"}} | ||
| tool = _make_tool(meta=meta) | ||
|
|
||
| internal = tool.to_fastmcp_internal_tool() | ||
| assert internal.meta == meta | ||
|
|
||
| def test_to_fastmcp_internal_tool_none_meta(self): | ||
| tool = _make_tool() | ||
|
|
||
| internal = tool.to_fastmcp_internal_tool() | ||
| assert internal.meta is None | ||
|
|
||
| def test_register_tools_passes_meta(self, mock_fastmcp): | ||
| mock_mcp, _ = mock_fastmcp | ||
| meta = {"ui": {"resourceUri": "ui://test/app.html"}} | ||
| tool = _make_tool(meta=meta) | ||
|
|
||
| generic_register_tools( | ||
| mock_mcp, | ||
| [tool], | ||
| disabled_tools=set(), | ||
| enabled_tools=None, | ||
| enabled_toolsets=set(), | ||
| disabled_toolsets=set(), | ||
| tool_to_toolset={FakeToolName.MY_TOOL: Toolset.DISCOVERY}, | ||
| ) | ||
|
|
||
| assert mock_mcp.tool_kwargs["my_tool"]["meta"] == meta | ||
|
|
||
| def test_register_tools_passes_none_meta(self, mock_fastmcp): | ||
| mock_mcp, _ = mock_fastmcp | ||
| tool = _make_tool() | ||
|
|
||
| generic_register_tools( | ||
| mock_mcp, | ||
| [tool], | ||
| disabled_tools=set(), | ||
| enabled_tools=None, | ||
| enabled_toolsets=set(), | ||
| disabled_toolsets=set(), | ||
| tool_to_toolset={FakeToolName.MY_TOOL: Toolset.DISCOVERY}, | ||
| ) | ||
|
|
||
| assert mock_mcp.tool_kwargs["my_tool"]["meta"] is None |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.