vizro-mcp 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vizro_mcp/server.py CHANGED
@@ -6,47 +6,44 @@ from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
  from typing import Any, Literal, Optional
8
8
 
9
+ import vizro
9
10
  import vizro.models as vm
10
11
  from mcp.server.fastmcp import FastMCP
11
- from pydantic import ValidationError
12
+ from pydantic import Field, ValidationError
12
13
  from vizro import Vizro
13
14
 
14
15
  from vizro_mcp._schemas import (
15
16
  AgGridEnhanced,
16
17
  ChartPlan,
17
- ContainerSimplified,
18
- DashboardSimplified,
19
- FilterSimplified,
18
+ FigureEnhanced,
20
19
  GraphEnhanced,
21
- PageSimplified,
22
- ParameterSimplified,
23
- TabsSimplified,
24
- get_overview_vizro_models,
25
- get_simple_dashboard_config,
26
20
  )
27
21
  from vizro_mcp._utils import (
22
+ CHART_INSTRUCTIONS,
28
23
  GAPMINDER,
29
24
  IRIS,
30
- SAMPLE_DASHBOARD_CONFIG,
25
+ LAYOUT_INSTRUCTIONS,
31
26
  STOCKS,
32
27
  TIPS,
33
28
  DFInfo,
34
29
  DFMetaData,
30
+ NoDefsGenerateJsonSchema,
35
31
  convert_github_url_to_raw,
36
32
  create_pycafe_url,
33
+ get_chart_prompt,
34
+ get_dashboard_instructions,
35
+ get_dashboard_prompt,
37
36
  get_dataframe_info,
38
37
  get_python_code_and_preview_link,
38
+ get_starter_dashboard_prompt,
39
39
  load_dataframe_by_format,
40
40
  path_or_url_check,
41
41
  )
42
42
 
43
- # PyCafe URL for Vizro snippets
44
- PYCAFE_URL = "https://py.cafe"
45
-
46
43
 
47
44
  @dataclass
48
- class ValidationResults:
49
- """Results of the validation tool."""
45
+ class ValidateResults:
46
+ """Results of validation tools."""
50
47
 
51
48
  valid: bool
52
49
  message: str
@@ -65,186 +62,136 @@ class DataAnalysisResults:
65
62
  df_metadata: Optional[DFMetaData]
66
63
 
67
64
 
68
- # TODO: what do I need to do here, as things are already set up?
69
- mcp = FastMCP(
70
- "MCP server to help create Vizro dashboards and charts.",
71
- )
72
-
73
-
74
- @mcp.tool()
75
- def get_sample_data_info(data_name: Literal["iris", "tips", "stocks", "gapminder"]) -> DFMetaData:
76
- """If user provides no data, use this tool to get sample data information.
65
+ @dataclass
66
+ class ModelJsonSchemaResults:
67
+ """Results of the get_model_json_schema tool."""
77
68
 
78
- Use the following data for the below purposes:
79
- - iris: mostly numerical with one categorical column, good for scatter, histogram, boxplot, etc.
80
- - tips: contains mix of numerical and categorical columns, good for bar, pie, etc.
81
- - stocks: stock prices, good for line, scatter, generally things that change over time
82
- - gapminder: demographic data, good for line, scatter, generally things with maps or many categories
69
+ model_name: str
70
+ json_schema: dict[str, Any]
71
+ additional_info: str
83
72
 
84
- Args:
85
- data_name: Name of the dataset to get sample data for
86
73
 
87
- Returns:
88
- Data info object containing information about the dataset.
89
- """
90
- if data_name == "iris":
91
- return IRIS
92
- elif data_name == "tips":
93
- return TIPS
94
- elif data_name == "stocks":
95
- return STOCKS
96
- elif data_name == "gapminder":
97
- return GAPMINDER
74
+ # TODO: check on https://github.com/modelcontextprotocol/python-sdk what new things are possible to do here
75
+ mcp = FastMCP(
76
+ name=f"MCP server to help create Vizro dashboards and charts. Server Vizro version: {vizro.__version__}",
77
+ )
98
78
 
99
79
 
100
80
  @mcp.tool()
101
- def validate_model_config(
102
- dashboard_config: dict[str, Any],
103
- data_infos: list[DFMetaData], # Should be Optional[..]=None, but Cursor complains..
104
- auto_open: bool = True,
105
- ) -> ValidationResults:
106
- """Validate Vizro model configuration. Run ALWAYS when you have a complete dashboard configuration.
107
-
108
- If successful, the tool will return the python code and, if it is a remote file, the py.cafe link to the chart.
109
- The PyCafe link will be automatically opened in your default browser if auto_open is True.
81
+ def get_vizro_chart_or_dashboard_plan(
82
+ user_plan: Literal["chart", "dashboard"] = Field(description="The type of Vizro thing the user wants to create"),
83
+ user_host: Literal["generic_host", "ide"] = Field(
84
+ description="The host the user is using, if 'ide' you can use the IDE/editor to run python code"
85
+ ),
86
+ advanced_mode: bool = Field(
87
+ default=False,
88
+ description="""Only call if you need to use custom CSS, custom components or custom actions.
89
+ No need to call this with advanced_mode=True if you need advanced charts,
90
+ use `custom_charts` in the `validate_dashboard_config` tool instead.""",
91
+ ),
92
+ ) -> str:
93
+ """Get instructions for creating a Vizro chart or dashboard. Call FIRST when asked to create Vizro things.
110
94
 
111
- Args:
112
- dashboard_config: Either a JSON string or a dictionary representing a Vizro dashboard model configuration
113
- data_infos: List of DFMetaData objects containing information about the data files
114
- auto_open: Whether to automatically open the PyCafe link in a browser
95
+ Must be ALWAYS called FIRST with advanced_mode=False, then call again with advanced_mode=True
96
+ if the JSON config does not suffice anymore.
115
97
 
116
98
  Returns:
117
- ValidationResults object with status and dashboard details
99
+ Instructions for creating a Vizro chart or dashboard
118
100
  """
119
- Vizro._reset()
120
-
121
- try:
122
- dashboard = vm.Dashboard.model_validate(dashboard_config)
123
- except ValidationError as e:
124
- return ValidationResults(
125
- valid=False,
126
- message=f"Validation Error: {e!s}",
127
- python_code="",
128
- pycafe_url=None,
129
- browser_opened=False,
130
- )
131
-
132
- else:
133
- result = get_python_code_and_preview_link(dashboard, data_infos)
134
-
135
- pycafe_url = result.pycafe_url if all(info.file_location_type == "remote" for info in data_infos) else None
136
- browser_opened = False
137
-
138
- if pycafe_url and auto_open:
139
- try:
140
- browser_opened = webbrowser.open(pycafe_url)
141
- except Exception:
142
- browser_opened = False
143
-
144
- return ValidationResults(
145
- valid=True,
146
- message="Configuration is valid for Dashboard!",
147
- python_code=result.python_code,
148
- pycafe_url=pycafe_url,
149
- browser_opened=browser_opened,
150
- )
151
-
152
- finally:
153
- Vizro._reset()
101
+ if user_plan == "chart":
102
+ return CHART_INSTRUCTIONS
103
+ elif user_plan == "dashboard":
104
+ return f"{get_dashboard_instructions(advanced_mode, user_host)}"
154
105
 
155
106
 
156
- @mcp.tool()
157
- def get_model_json_schema(model_name: str) -> dict[str, Any]:
107
+ @mcp.tool(description=f"Get the JSON schema for the specified Vizro model. Server Vizro version: {vizro.__version__}")
108
+ def get_model_json_schema(
109
+ model_name: str = Field(
110
+ description="Name of the Vizro model to get schema for (e.g., 'Card', 'Dashboard', 'Page')"
111
+ ),
112
+ ) -> ModelJsonSchemaResults:
158
113
  """Get the JSON schema for the specified Vizro model.
159
114
 
160
- Args:
161
- model_name: Name of the Vizro model to get schema for (e.g., 'Card', 'Dashboard', 'Page')
162
-
163
115
  Returns:
164
116
  JSON schema of the requested Vizro model
165
117
  """
166
- # Dictionary mapping model names to their simplified versions
118
+ if not hasattr(vm, model_name):
119
+ return ModelJsonSchemaResults(
120
+ model_name=model_name,
121
+ json_schema={},
122
+ additional_info=f"Model '{model_name}' not found in vizro.models",
123
+ )
124
+
167
125
  modified_models = {
168
- "Page": PageSimplified,
169
- "Dashboard": DashboardSimplified,
170
126
  "Graph": GraphEnhanced,
171
127
  "AgGrid": AgGridEnhanced,
172
128
  "Table": AgGridEnhanced,
173
- "Tabs": TabsSimplified,
174
- "Container": ContainerSimplified,
175
- "Filter": FilterSimplified,
176
- "Parameter": ParameterSimplified,
129
+ "Figure": FigureEnhanced,
177
130
  }
178
131
 
179
- # Check if model_name is in the simplified models dictionary
180
132
  if model_name in modified_models:
181
- return modified_models[model_name].model_json_schema()
182
-
183
- # Check if model exists in vizro.models
184
- if not hasattr(vm, model_name):
185
- return {"error": f"Model '{model_name}' not found in vizro.models"}
133
+ return ModelJsonSchemaResults(
134
+ model_name=model_name,
135
+ json_schema=modified_models[model_name].model_json_schema(schema_generator=NoDefsGenerateJsonSchema),
136
+ additional_info="""LLM must remember to replace `$ref` with the actual config. Request the schema of
137
+ that model if necessary. Do NOT forget to call `validate_dashboard_config` after each iteration.""",
138
+ )
139
+ deprecated_models = {"filter_interaction": "set_control", "Layout": "Grid"}
140
+ if model_name in deprecated_models:
141
+ return ModelJsonSchemaResults(
142
+ model_name=model_name,
143
+ json_schema={},
144
+ additional_info=f"Model '{model_name}' is deprecated. Use {deprecated_models[model_name]} instead.",
145
+ )
186
146
 
187
- # Get schema for standard model
188
147
  model_class = getattr(vm, model_name)
189
- return model_class.model_json_schema()
148
+ if model_name in {"Grid", "Flex"}:
149
+ return ModelJsonSchemaResults(
150
+ model_name=model_name,
151
+ json_schema=model_class.model_json_schema(schema_generator=NoDefsGenerateJsonSchema),
152
+ additional_info=LAYOUT_INSTRUCTIONS,
153
+ )
154
+
155
+ return ModelJsonSchemaResults(
156
+ model_name=model_name,
157
+ json_schema=model_class.model_json_schema(schema_generator=NoDefsGenerateJsonSchema),
158
+ additional_info="""LLM must remember to replace `$ref` with the actual config. Request the schema of
159
+ that model if necessary. Do NOT forget to call `validate_dashboard_config` after each iteration.""",
160
+ )
190
161
 
191
162
 
192
163
  @mcp.tool()
193
- def get_vizro_chart_or_dashboard_plan(user_plan: Literal["chart", "dashboard"]) -> str:
194
- """Get instructions for creating a Vizro chart or dashboard. Call FIRST when asked to create Vizro things."""
195
- if user_plan == "chart":
196
- return """
197
- IMPORTANT:
198
- - KEEP IT SIMPLE: rather than iterating yourself, ask the user for more instructions
199
- - ALWAYS VALIDATE:if you iterate over a valid produced solution, make sure to ALWAYS call the
200
- validate_chart_code tool to validate the chart code, display the figure code to the user
201
- - DO NOT modify the background (with plot_bgcolor) or color sequences unless explicitly asked for
202
-
203
- Instructions for creating a Vizro chart:
204
- - analyze the datasets needed for the chart using the load_and_analyze_data tool - the most important
205
- information here are the column names and column types
206
- - if the user provides no data, but you need to display a chart or table, use the get_sample_data_info
207
- tool to get sample data information
208
- - create a chart using plotly express and/or plotly graph objects, and call the function `custom_chart`
209
- - call the validate_chart_code tool to validate the chart code, display the figure code to the user (as artifact)
210
- - do NOT call any other tool after, especially do NOT create a dashboard
211
- """
212
- elif user_plan == "dashboard":
213
- return f"""
214
- IMPORTANT:
215
- - KEEP IT SIMPLE: rather than iterating yourself, ask the user for more instructions
216
- - ALWAYS VALIDATE:if you iterate over a valid produced solution, make sure to ALWAYS call the
217
- validate_model_config tool again to ensure the solution is still valid
218
- - DO NOT show any code or config to the user until you have validated the solution, do not say you are preparing
219
- a solution, just do it and validate it
220
- - IF STUCK: try enquiring the schema of the component in question
221
-
222
-
223
- Instructions for creating a Vizro dashboard:
224
- - IF the user has no plan (ie no components or pages), use the config at the bottom of this prompt
225
- and validate that solution without any additions, OTHERWISE:
226
- - analyze the datasets needed for the dashboard using the load_and_analyze_data tool - the most
227
- important information here are the column names and column types
228
- - if the user provides no data, but you need to display a chart or table, use the get_sample_data_info
229
- tool to get sample data information
230
- - make a plan of what components you would like to use, then request all necessary schemas
231
- using the get_model_json_schema tool
232
- - assemble your components into a page, then add the page or pages to a dashboard, DO NOT show config or code
233
- to the user until you have validated the solution
234
- - ALWAYS validate the dashboard configuration using the validate_model_config tool
235
- - if you display any code artifact, you must use the above created code, do not add new config to it
236
-
237
- Models you can use:
238
- {get_overview_vizro_models()}
239
-
240
- Very simple dashboard config:
241
- {get_simple_dashboard_config()}
164
+ def get_sample_data_info(
165
+ data_name: Literal["iris", "tips", "stocks", "gapminder"] = Field(
166
+ description="Name of the dataset to get sample data for"
167
+ ),
168
+ ) -> DFMetaData:
169
+ """If user provides no data, use this tool to get sample data information.
170
+
171
+ Use the following data for the below purposes:
172
+ - iris: mostly numerical with one categorical column, good for scatter, histogram, boxplot, etc.
173
+ - tips: contains mix of numerical and categorical columns, good for bar, pie, etc.
174
+ - stocks: stock prices, good for line, scatter, generally things that change over time
175
+ - gapminder: demographic data, good for line, scatter, generally things with maps or many categories
176
+
177
+ Returns:
178
+ Data info object containing information about the dataset.
242
179
  """
180
+ if data_name == "iris":
181
+ return IRIS
182
+ elif data_name == "tips":
183
+ return TIPS
184
+ elif data_name == "stocks":
185
+ return STOCKS
186
+ elif data_name == "gapminder":
187
+ return GAPMINDER
243
188
 
244
189
 
245
190
  @mcp.tool()
246
- def load_and_analyze_data(path_or_url: str) -> DataAnalysisResults:
247
- """Load data from various file formats into a pandas DataFrame and analyze its structure.
191
+ def load_and_analyze_data(
192
+ path_or_url: str = Field(description="Absolute (important!) local file path or URL to a data file"),
193
+ ) -> DataAnalysisResults:
194
+ """Use to understand local or remote data files. Must be called with absolute paths or URLs.
248
195
 
249
196
  Supported formats:
250
197
  - CSV (.csv)
@@ -254,9 +201,6 @@ def load_and_analyze_data(path_or_url: str) -> DataAnalysisResults:
254
201
  - OpenDocument Spreadsheet (.ods)
255
202
  - Parquet (.parquet)
256
203
 
257
- Args:
258
- path_or_url: Local file path or URL to a data file
259
-
260
204
  Returns:
261
205
  DataAnalysisResults object containing DataFrame information and metadata
262
206
  """
@@ -276,7 +220,14 @@ def load_and_analyze_data(path_or_url: str) -> DataAnalysisResults:
276
220
  df, read_fn = load_dataframe_by_format(processed_path_or_url, mime_type)
277
221
 
278
222
  except Exception as e:
279
- return DataAnalysisResults(valid=False, message=f"Failed to load data: {e!s}", df_info=None, df_metadata=None)
223
+ return DataAnalysisResults(
224
+ valid=False,
225
+ message=f"""Failed to load data: {e!s}. Remember to use the ABSOLUTE path or URL!
226
+ Alternatively, you can use any data analysis means available to you. Most important information are the column names and
227
+ column types for passing along to the `validate_dashboard_config` or `validate_chart_code` tools.""",
228
+ df_info=None,
229
+ df_metadata=None,
230
+ )
280
231
 
281
232
  df_info = get_dataframe_info(df)
282
233
  df_metadata = DFMetaData(
@@ -289,61 +240,95 @@ def load_and_analyze_data(path_or_url: str) -> DataAnalysisResults:
289
240
  return DataAnalysisResults(valid=True, message="Data loaded successfully", df_info=df_info, df_metadata=df_metadata)
290
241
 
291
242
 
243
+ # TODO: Additional things we could validate:
244
+ # - data_infos: check we are referring to the correct dataframe, or at least A DF
245
+ @mcp.tool()
246
+ def validate_dashboard_config(
247
+ dashboard_config: dict[str, Any] = Field(
248
+ description="Either a JSON string or a dictionary representing a Vizro dashboard model configuration"
249
+ ),
250
+ data_infos: list[DFMetaData] = Field(
251
+ description="List of DFMetaData objects containing information about the data files"
252
+ ),
253
+ custom_charts: list[ChartPlan] = Field(
254
+ description="List of ChartPlan objects containing information about the custom charts in the dashboard"
255
+ ),
256
+ auto_open: bool = Field(default=True, description="Whether to automatically open the PyCafe link in a browser"),
257
+ ) -> ValidateResults:
258
+ """Validate Vizro model configuration. Run ALWAYS when you have a complete dashboard configuration.
259
+
260
+ If successful, the tool will return the python code and, if it is a remote file, the py.cafe link to the chart.
261
+ The PyCafe link will be automatically opened in your default browser if auto_open is True.
262
+
263
+ Returns:
264
+ ValidationResults object with status and dashboard details
265
+ """
266
+ Vizro._reset()
267
+
268
+ try:
269
+ dashboard = vm.Dashboard.model_validate(
270
+ dashboard_config,
271
+ context={"allow_undefined_captured_callable": [custom_chart.chart_name for custom_chart in custom_charts]},
272
+ )
273
+ except ValidationError as e:
274
+ return ValidateResults(
275
+ valid=False,
276
+ message=f"""Validation Error: {e!s}. Fix the error and call this tool again.
277
+ Calling `get_model_json_schema` may help.""",
278
+ python_code="",
279
+ pycafe_url=None,
280
+ browser_opened=False,
281
+ )
282
+
283
+ else:
284
+ code_link = get_python_code_and_preview_link(dashboard, data_infos, custom_charts)
285
+
286
+ pycafe_url = code_link.pycafe_url if all(info.file_location_type == "remote" for info in data_infos) else None
287
+ browser_opened = False
288
+
289
+ if pycafe_url and auto_open:
290
+ try:
291
+ browser_opened = webbrowser.open(pycafe_url)
292
+ except Exception:
293
+ browser_opened = False
294
+
295
+ return ValidateResults(
296
+ valid=True,
297
+ message="""Configuration is valid for Dashboard! Do not forget to call this tool again after each iteration.
298
+ If you are creating an `app.py` file, you MUST use the code from the validation tool, do not modify it, watch out for
299
+ differences to previous `app.py`""",
300
+ python_code=code_link.python_code,
301
+ pycafe_url=pycafe_url,
302
+ browser_opened=browser_opened,
303
+ )
304
+
305
+ finally:
306
+ Vizro._reset()
307
+
308
+
292
309
  @mcp.prompt()
293
310
  def create_starter_dashboard():
294
311
  """Prompt template for getting started with Vizro."""
295
- content = f"""
296
- Create a super simple Vizro dashboard with one page and one chart and one filter:
297
- - No need to call any tools except for validate_model_config
298
- - Call this tool with the precise config as shown below
299
- - The PyCafe link will be automatically opened in your default browser
300
- - THEN show the python code after validation, but do not show the PyCafe link
301
- - Be concise, do not explain anything else, just create the dashboard
302
- - Finally ask the user what they would like to do next, then you can call other tools to get more information,
303
- you should then start with the get_chart_or_dashboard_plan tool
304
-
305
- {SAMPLE_DASHBOARD_CONFIG}
306
- """
307
- return content
312
+ return get_starter_dashboard_prompt()
308
313
 
309
314
 
310
315
  @mcp.prompt()
311
- def create_eda_dashboard(
312
- file_path_or_url: str,
316
+ def create_dashboard(
317
+ file_path_or_url: str = Field(description="The absolute path or URL to the data file you want to use."),
318
+ context: Optional[str] = Field(default=None, description="(Optional) Describe the dashboard you want to create."),
313
319
  ) -> str:
314
320
  """Prompt template for creating an EDA dashboard based on one dataset."""
315
- content = f"""
316
- Create an EDA dashboard based on the following dataset:{file_path_or_url}. Proceed as follows:
317
- 1. Analyze the data using the load_and_analyze_data tool first, passing the file path or github url {file_path_or_url}
318
- to the tool.
319
- 2. Create a dashboard with 4 pages:
320
- - Page 1: Summary of the dataset using the Card component and the dataset itself using the plain AgGrid component.
321
- - Page 2: Visualizing the distribution of all numeric columns using the Graph component with a histogram.
322
- - use a Parameter that targets the Graph component and the x argument, and you can select the column to
323
- be displayed
324
- - IMPORTANT:remember that you target the chart like: <graph_id>.x and NOT <graph_id>.figure.x
325
- - do not use any color schemes etc.
326
- - add filters for all categorical columns
327
- - Page 3: Visualizing the correlation between all numeric columns using the Graph component with a scatter plot.
328
- - Page 4: Two interesting charts side by side, use the Graph component for this. Make sure they look good
329
- but do not try something beyond the scope of plotly express
330
- """
331
- return content
321
+ return get_dashboard_prompt(file_path_or_url, context)
332
322
 
333
323
 
334
324
  @mcp.tool()
335
325
  def validate_chart_code(
336
- chart_config: ChartPlan,
337
- data_info: DFMetaData,
338
- auto_open: bool = True,
339
- ) -> ValidationResults:
326
+ chart_config: ChartPlan = Field(description="A ChartPlan object with the chart configuration"),
327
+ data_info: DFMetaData = Field(description="Metadata for the dataset to be used in the chart"),
328
+ auto_open: bool = Field(default=True, description="Whether to automatically open the PyCafe link in a browser"),
329
+ ) -> ValidateResults:
340
330
  """Validate the chart code created by the user and optionally open the PyCafe link in a browser.
341
331
 
342
- Args:
343
- chart_config: A ChartPlan object with the chart configuration
344
- data_info: Metadata for the dataset to be used in the chart
345
- auto_open: Whether to automatically open the PyCafe link in a browser
346
-
347
332
  Returns:
348
333
  ValidationResults object with status and dashboard details
349
334
  """
@@ -352,7 +337,7 @@ def validate_chart_code(
352
337
  try:
353
338
  chart_plan_obj = ChartPlan.model_validate(chart_config)
354
339
  except ValidationError as e:
355
- return ValidationResults(
340
+ return ValidateResults(
356
341
  valid=False,
357
342
  message=f"Validation Error: {e!s}",
358
343
  python_code="",
@@ -372,7 +357,7 @@ def validate_chart_code(
372
357
  except Exception:
373
358
  browser_opened = False
374
359
 
375
- return ValidationResults(
360
+ return ValidateResults(
376
361
  valid=True,
377
362
  message="Chart only dashboard created successfully!",
378
363
  python_code=chart_plan_obj.get_chart_code(vizro=True),
@@ -386,15 +371,8 @@ def validate_chart_code(
386
371
 
387
372
  @mcp.prompt()
388
373
  def create_vizro_chart(
389
- chart_type: str,
390
- file_path_or_url: Optional[str] = None,
374
+ file_path_or_url: str = Field(description="The absolute path or URL to the data file you want to use."),
375
+ context: Optional[str] = Field(default=None, description="(Optional) Describe the chart you want to create."),
391
376
  ) -> str:
392
377
  """Prompt template for creating a Vizro chart."""
393
- content = f"""
394
- - Create a chart using the following chart type: {chart_type}.
395
- - You MUST name the function containing the fig `custom_chart`
396
- - Make sure to analyze the data using the load_and_analyze_data tool first, passing the file path or github url
397
- {file_path_or_url} OR choose the most appropriate sample data using the get_sample_data_info tool.
398
- Then you MUST use the validate_chart_code tool to validate the chart code.
399
- """
400
- return content
378
+ return get_chart_prompt(file_path_or_url, context)