Skip to content

Commit dad4692

Browse files
feat(mcp): add configurable branding for MCP service (apache#36033)
1 parent 85413f2 commit dad4692

File tree

3 files changed

+188
-13
lines changed

3 files changed

+188
-13
lines changed

superset/mcp_service/app.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,20 @@
2929

3030
logger = logging.getLogger(__name__)
3131

32-
# Default instructions for the Superset MCP service
33-
DEFAULT_INSTRUCTIONS = """
34-
You are connected to the Apache Superset MCP (Model Context Protocol) service.
35-
This service provides programmatic access to Superset dashboards, charts, datasets,
32+
33+
def get_default_instructions(branding: str = "Apache Superset") -> str:
34+
"""Get default instructions with configurable branding.
35+
36+
Args:
37+
branding: Product name to use in instructions
38+
(e.g., "ACME Analytics", "Apache Superset")
39+
40+
Returns:
41+
Formatted instructions string with branding applied
42+
"""
43+
return f"""
44+
You are connected to the {branding} MCP (Model Context Protocol) service.
45+
This service provides programmatic access to {branding} dashboards, charts, datasets,
3646
SQL Lab, and instance metadata via a comprehensive set of tools.
3747
3848
Available tools:
@@ -107,9 +117,9 @@
107117
108118
Query Examples:
109119
- List all interactive tables:
110-
filters=[{"col": "viz_type", "opr": "in", "value": ["table", "pivot_table_v2"]}]
120+
filters=[{{"col": "viz_type", "opr": "in", "value": ["table", "pivot_table_v2"]}}]
111121
- List time series charts:
112-
filters=[{"col": "viz_type", "opr": "sw", "value": "echarts_timeseries"}]
122+
filters=[{{"col": "viz_type", "opr": "sw", "value": "echarts_timeseries"}}]
113123
- Search by name: search="sales"
114124
115125
General usage tips:
@@ -124,6 +134,10 @@
124134
"""
125135

126136

137+
# For backwards compatibility, keep DEFAULT_INSTRUCTIONS pointing to default branding
138+
DEFAULT_INSTRUCTIONS = get_default_instructions()
139+
140+
127141
def _build_mcp_kwargs(
128142
name: str,
129143
instructions: str,
@@ -185,6 +199,7 @@ def _log_instance_creation(
185199
def create_mcp_app(
186200
name: str = "Superset MCP Server",
187201
instructions: str | None = None,
202+
branding: str | None = None,
188203
auth: Any | None = None,
189204
lifespan: Callable[..., Any] | None = None,
190205
tools: List[Any] | None = None,
@@ -203,6 +218,7 @@ def create_mcp_app(
203218
Args:
204219
name: Human-readable server name
205220
instructions: Server description and usage instructions
221+
branding: Product name for instructions (e.g., "ACME Analytics")
206222
auth: Authentication provider for securing HTTP transports
207223
lifespan: Async context manager for startup/shutdown logic
208224
tools: List of tools or functions to add to the server
@@ -216,7 +232,11 @@ def create_mcp_app(
216232
"""
217233
# Use default instructions if none provided
218234
if instructions is None:
219-
instructions = DEFAULT_INSTRUCTIONS
235+
# If branding is provided, use it to generate instructions
236+
if branding is not None:
237+
instructions = get_default_instructions(branding)
238+
else:
239+
instructions = DEFAULT_INSTRUCTIONS
220240

221241
# Build FastMCP constructor arguments
222242
mcp_kwargs = _build_mcp_kwargs(
@@ -290,7 +310,7 @@ def create_mcp_app(
290310

291311

292312
def init_fastmcp_server(
293-
name: str = "Superset MCP Server",
313+
name: str | None = None,
294314
instructions: str | None = None,
295315
auth: Any | None = None,
296316
lifespan: Callable[..., Any] | None = None,
@@ -308,16 +328,33 @@ def init_fastmcp_server(
308328
a new instance will be created with those settings.
309329
310330
Args:
311-
Same as create_mcp_app()
331+
name: Server name (defaults to "{APP_NAME} MCP Server")
332+
instructions: Custom instructions (defaults to branded with APP_NAME)
333+
auth, lifespan, tools, include_tags, exclude_tags, config: FastMCP configuration
334+
**kwargs: Additional FastMCP configuration
312335
313336
Returns:
314337
FastMCP instance (either the global one or a new custom one)
315338
"""
339+
# Read branding from Flask config's APP_NAME
340+
from superset.mcp_service.flask_singleton import app as flask_app
341+
342+
# Derive branding from Superset's APP_NAME config (defaults to "Superset")
343+
app_name = flask_app.config.get("APP_NAME", "Superset")
344+
branding = app_name
345+
default_name = f"{app_name} MCP Server"
346+
347+
# Apply branding defaults if not explicitly provided
348+
if name is None:
349+
name = default_name
350+
if instructions is None:
351+
instructions = get_default_instructions(branding)
352+
316353
# If any custom parameters are provided, create a new instance
317354
custom_params_provided = any(
318355
[
319-
name != "Superset MCP Server",
320-
instructions is not None,
356+
name != default_name,
357+
instructions != get_default_instructions(branding),
321358
auth is not None,
322359
lifespan is not None,
323360
tools is not None,

superset/mcp_service/mcp_config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@
6262

6363
# FastMCP Factory Configuration
6464
MCP_FACTORY_CONFIG = {
65-
"name": "Superset MCP Server",
66-
"instructions": None, # Will use default from app.py
65+
"name": None, # Will derive from APP_NAME in app.py
66+
"branding": None, # Will derive from APP_NAME in app.py
67+
"instructions": None, # Will use default from app.py (parameterized with branding)
6768
"auth": None, # No authentication by default
6869
"lifespan": None, # No custom lifespan
6970
"tools": None, # Auto-discover tools
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""Tests for MCP service configuration and branding."""
19+
20+
from unittest.mock import MagicMock, patch
21+
22+
from superset.mcp_service.app import get_default_instructions, init_fastmcp_server
23+
24+
25+
def test_get_default_instructions_with_default_branding():
26+
"""Test that default branding produces Apache Superset in instructions."""
27+
instructions = get_default_instructions()
28+
29+
assert "Apache Superset" in instructions
30+
assert "Apache Superset MCP" in instructions
31+
assert "model context protocol" in instructions.lower()
32+
33+
34+
def test_get_default_instructions_with_custom_branding():
35+
"""Test that custom branding is reflected in instructions."""
36+
custom_branding = "ACME Analytics"
37+
instructions = get_default_instructions(branding=custom_branding)
38+
39+
assert custom_branding in instructions
40+
assert f"{custom_branding} MCP" in instructions
41+
# Should not contain default Apache Superset branding
42+
assert "Apache Superset" not in instructions
43+
44+
45+
def test_get_default_instructions_with_enterprise_branding():
46+
"""Test instructions with enterprise/white-label branding."""
47+
enterprise_branding = "DataViz Platform"
48+
instructions = get_default_instructions(branding=enterprise_branding)
49+
50+
assert enterprise_branding in instructions
51+
assert f"{enterprise_branding} MCP" in instructions
52+
# Verify it contains expected tool documentation
53+
assert "list_dashboards" in instructions
54+
assert "list_charts" in instructions
55+
assert "execute_sql" in instructions
56+
57+
58+
def test_init_fastmcp_server_with_default_app_name():
59+
"""Test that default APP_NAME produces Superset branding."""
60+
# Mock Flask app config with default APP_NAME
61+
mock_flask_app = MagicMock()
62+
mock_flask_app.config.get.return_value = "Superset"
63+
64+
# Patch at the import location to avoid actual Flask app creation
65+
with patch.dict(
66+
"sys.modules",
67+
{"superset.mcp_service.flask_singleton": MagicMock(app=mock_flask_app)},
68+
):
69+
with patch("superset.mcp_service.app.create_mcp_app") as mock_create:
70+
mock_mcp = MagicMock()
71+
mock_create.return_value = mock_mcp
72+
73+
# Call with custom name to force create_mcp_app path
74+
init_fastmcp_server(name="Custom Name")
75+
76+
# Verify create_mcp_app was called
77+
assert mock_create.called
78+
# Verify instructions use Superset branding (not Apache Superset)
79+
call_kwargs = mock_create.call_args[1]
80+
assert "Superset MCP" in call_kwargs["instructions"]
81+
assert "Superset dashboards" in call_kwargs["instructions"]
82+
83+
84+
def test_init_fastmcp_server_with_custom_app_name():
85+
"""Test that custom APP_NAME produces branded instructions."""
86+
custom_app_name = "ACME Analytics"
87+
# Mock Flask app config with custom APP_NAME
88+
mock_flask_app = MagicMock()
89+
mock_flask_app.config.get.return_value = custom_app_name
90+
91+
# Patch at the import location to avoid actual Flask app creation
92+
with patch.dict(
93+
"sys.modules",
94+
{"superset.mcp_service.flask_singleton": MagicMock(app=mock_flask_app)},
95+
):
96+
with patch("superset.mcp_service.app.create_mcp_app") as mock_create:
97+
mock_mcp = MagicMock()
98+
mock_create.return_value = mock_mcp
99+
100+
# Call with custom name to force create_mcp_app path
101+
init_fastmcp_server(name="Custom Name")
102+
103+
# Verify create_mcp_app was called
104+
assert mock_create.called
105+
# Verify instructions use custom branding
106+
call_kwargs = mock_create.call_args[1]
107+
assert custom_app_name in call_kwargs["instructions"]
108+
# Should not contain default Apache Superset branding
109+
assert "Apache Superset" not in call_kwargs["instructions"]
110+
111+
112+
def test_init_fastmcp_server_derives_server_name_from_app_name():
113+
"""Test that server name is derived from APP_NAME."""
114+
custom_app_name = "DataViz Platform"
115+
expected_server_name = f"{custom_app_name} MCP Server"
116+
117+
# Mock Flask app config
118+
mock_flask_app = MagicMock()
119+
mock_flask_app.config.get.return_value = custom_app_name
120+
121+
# Patch at the import location to avoid actual Flask app creation
122+
with patch.dict(
123+
"sys.modules",
124+
{"superset.mcp_service.flask_singleton": MagicMock(app=mock_flask_app)},
125+
):
126+
with patch("superset.mcp_service.app.create_mcp_app") as mock_create:
127+
mock_mcp = MagicMock()
128+
mock_create.return_value = mock_mcp
129+
130+
# Call without name parameter (should use default derived name)
131+
# Force custom params by passing instructions
132+
init_fastmcp_server(instructions="custom")
133+
134+
# Verify create_mcp_app was called with derived name
135+
assert mock_create.called
136+
call_kwargs = mock_create.call_args[1]
137+
assert call_kwargs["name"] == expected_server_name

0 commit comments

Comments
 (0)