vizro-mcp 0.1.1__py3-none-any.whl → 0.1.2__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
@@ -8,45 +8,39 @@ from typing import Any, Literal, Optional
8
8
 
9
9
  import vizro.models as vm
10
10
  from mcp.server.fastmcp import FastMCP
11
- from pydantic import ValidationError
11
+ from pydantic import Field, ValidationError
12
12
  from vizro import Vizro
13
13
 
14
14
  from vizro_mcp._schemas import (
15
15
  AgGridEnhanced,
16
16
  ChartPlan,
17
- ContainerSimplified,
18
- DashboardSimplified,
19
- FilterSimplified,
20
17
  GraphEnhanced,
21
- PageSimplified,
22
- ParameterSimplified,
23
- TabsSimplified,
24
- get_overview_vizro_models,
25
- get_simple_dashboard_config,
26
18
  )
27
19
  from vizro_mcp._utils import (
20
+ CHART_INSTRUCTIONS,
28
21
  GAPMINDER,
29
22
  IRIS,
30
- SAMPLE_DASHBOARD_CONFIG,
31
23
  STOCKS,
32
24
  TIPS,
33
25
  DFInfo,
34
26
  DFMetaData,
27
+ NoDefsGenerateJsonSchema,
35
28
  convert_github_url_to_raw,
36
29
  create_pycafe_url,
30
+ get_chart_prompt,
31
+ get_dashboard_instructions,
32
+ get_dashboard_prompt,
37
33
  get_dataframe_info,
38
34
  get_python_code_and_preview_link,
35
+ get_starter_dashboard_prompt,
39
36
  load_dataframe_by_format,
40
37
  path_or_url_check,
41
38
  )
42
39
 
43
- # PyCafe URL for Vizro snippets
44
- PYCAFE_URL = "https://py.cafe"
45
-
46
40
 
47
41
  @dataclass
48
- class ValidationResults:
49
- """Results of the validation tool."""
42
+ class ValidateResults:
43
+ """Results of validation tools."""
50
44
 
51
45
  valid: bool
52
46
  message: str
@@ -65,96 +59,50 @@ class DataAnalysisResults:
65
59
  df_metadata: Optional[DFMetaData]
66
60
 
67
61
 
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
- )
62
+ @dataclass
63
+ class ModelJsonSchemaResults:
64
+ """Results of the get_model_json_schema tool."""
72
65
 
66
+ model_name: str
67
+ json_schema: dict[str, Any]
68
+ additional_info: str
73
69
 
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.
77
70
 
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
83
-
84
- Args:
85
- data_name: Name of the dataset to get sample data for
86
-
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
71
+ # TODO: check on https://github.com/modelcontextprotocol/python-sdk what new things are possible to do here
72
+ mcp = FastMCP(
73
+ "MCP server to help create Vizro dashboards and charts.",
74
+ )
98
75
 
99
76
 
100
77
  @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.
78
+ def get_vizro_chart_or_dashboard_plan(
79
+ user_plan: Literal["chart", "dashboard"],
80
+ user_host: Literal["generic_host", "ide"],
81
+ advanced_mode: bool = False,
82
+ ) -> str:
83
+ """Get instructions for creating a Vizro chart or dashboard. Call FIRST when asked to create Vizro things.
107
84
 
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.
85
+ Must be ALWAYS called FIRST with advanced_mode=False, then call again with advanced_mode=True
86
+ if the JSON config does not suffice anymore.
110
87
 
111
88
  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
89
+ user_plan: The type of Vizro thing the user wants to create
90
+ user_host: The host the user is using, if "ide" you can use the IDE/editor to run python code
91
+ advanced_mode: Only call if you need to use custom CSS, custom components or custom actions.
92
+ No need to call this with advanced_mode=True if you need advanced charts, use `custom_charts` in
93
+ the `validate_dashboard_config` tool instead.
115
94
 
116
95
  Returns:
117
- ValidationResults object with status and dashboard details
96
+ Instructions for creating a Vizro chart or dashboard
118
97
  """
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()
98
+ if user_plan == "chart":
99
+ return CHART_INSTRUCTIONS
100
+ elif user_plan == "dashboard":
101
+ return f"{get_dashboard_instructions(advanced_mode, user_host)}"
154
102
 
155
103
 
156
104
  @mcp.tool()
157
- def get_model_json_schema(model_name: str) -> dict[str, Any]:
105
+ def get_model_json_schema(model_name: str) -> ModelJsonSchemaResults:
158
106
  """Get the JSON schema for the specified Vizro model.
159
107
 
160
108
  Args:
@@ -163,88 +111,65 @@ def get_model_json_schema(model_name: str) -> dict[str, Any]:
163
111
  Returns:
164
112
  JSON schema of the requested Vizro model
165
113
  """
166
- # Dictionary mapping model names to their simplified versions
167
114
  modified_models = {
168
- "Page": PageSimplified,
169
- "Dashboard": DashboardSimplified,
170
115
  "Graph": GraphEnhanced,
171
116
  "AgGrid": AgGridEnhanced,
172
117
  "Table": AgGridEnhanced,
173
- "Tabs": TabsSimplified,
174
- "Container": ContainerSimplified,
175
- "Filter": FilterSimplified,
176
- "Parameter": ParameterSimplified,
177
118
  }
178
119
 
179
- # Check if model_name is in the simplified models dictionary
180
120
  if model_name in modified_models:
181
- return modified_models[model_name].model_json_schema()
121
+ return ModelJsonSchemaResults(
122
+ model_name=model_name,
123
+ json_schema=modified_models[model_name].model_json_schema(schema_generator=NoDefsGenerateJsonSchema),
124
+ additional_info="""LLM must remember to replace `$ref` with the actual config. Request the schema of
125
+ that model if necessary. Do NOT forget to call `validate_dashboard_config` after each iteration.""",
126
+ )
182
127
 
183
- # Check if model exists in vizro.models
184
128
  if not hasattr(vm, model_name):
185
- return {"error": f"Model '{model_name}' not found in vizro.models"}
129
+ return ModelJsonSchemaResults(
130
+ model_name=model_name,
131
+ json_schema={},
132
+ additional_info=f"Model '{model_name}' not found in vizro.models",
133
+ )
186
134
 
187
- # Get schema for standard model
188
135
  model_class = getattr(vm, model_name)
189
- return model_class.model_json_schema()
136
+ return ModelJsonSchemaResults(
137
+ model_name=model_name,
138
+ json_schema=model_class.model_json_schema(schema_generator=NoDefsGenerateJsonSchema),
139
+ additional_info="""LLM must remember to replace `$ref` with the actual config. Request the schema of
140
+ that model if necessary. Do NOT forget to call `validate_dashboard_config` after each iteration.""",
141
+ )
190
142
 
191
143
 
192
144
  @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()}
145
+ def get_sample_data_info(data_name: Literal["iris", "tips", "stocks", "gapminder"]) -> DFMetaData:
146
+ """If user provides no data, use this tool to get sample data information.
147
+
148
+ Use the following data for the below purposes:
149
+ - iris: mostly numerical with one categorical column, good for scatter, histogram, boxplot, etc.
150
+ - tips: contains mix of numerical and categorical columns, good for bar, pie, etc.
151
+ - stocks: stock prices, good for line, scatter, generally things that change over time
152
+ - gapminder: demographic data, good for line, scatter, generally things with maps or many categories
153
+
154
+ Args:
155
+ data_name: Name of the dataset to get sample data for
156
+
157
+ Returns:
158
+ Data info object containing information about the dataset.
242
159
  """
160
+ if data_name == "iris":
161
+ return IRIS
162
+ elif data_name == "tips":
163
+ return TIPS
164
+ elif data_name == "stocks":
165
+ return STOCKS
166
+ elif data_name == "gapminder":
167
+ return GAPMINDER
243
168
 
244
169
 
245
170
  @mcp.tool()
246
171
  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.
172
+ """Use to understand local or remote data files. Must be called with absolute paths or URLs.
248
173
 
249
174
  Supported formats:
250
175
  - CSV (.csv)
@@ -255,7 +180,7 @@ def load_and_analyze_data(path_or_url: str) -> DataAnalysisResults:
255
180
  - Parquet (.parquet)
256
181
 
257
182
  Args:
258
- path_or_url: Local file path or URL to a data file
183
+ path_or_url: Absolute (important!) local file path or URL to a data file
259
184
 
260
185
  Returns:
261
186
  DataAnalysisResults object containing DataFrame information and metadata
@@ -276,7 +201,14 @@ def load_and_analyze_data(path_or_url: str) -> DataAnalysisResults:
276
201
  df, read_fn = load_dataframe_by_format(processed_path_or_url, mime_type)
277
202
 
278
203
  except Exception as e:
279
- return DataAnalysisResults(valid=False, message=f"Failed to load data: {e!s}", df_info=None, df_metadata=None)
204
+ return DataAnalysisResults(
205
+ valid=False,
206
+ message=f"""Failed to load data: {e!s}. Remember to use the ABSOLUTE path or URL!
207
+ Alternatively, you can use any data analysis means available to you. Most important information are the column names and
208
+ column types for passing along to the `validate_dashboard_config` or `validate_chart_code` tools.""",
209
+ df_info=None,
210
+ df_metadata=None,
211
+ )
280
212
 
281
213
  df_info = get_dataframe_info(df)
282
214
  df_metadata = DFMetaData(
@@ -289,46 +221,85 @@ def load_and_analyze_data(path_or_url: str) -> DataAnalysisResults:
289
221
  return DataAnalysisResults(valid=True, message="Data loaded successfully", df_info=df_info, df_metadata=df_metadata)
290
222
 
291
223
 
224
+ # TODO: Additional things we could validate:
225
+ # - data_infos: check we are referring to the correct dataframe, or at least A DF
226
+ @mcp.tool()
227
+ def validate_dashboard_config(
228
+ dashboard_config: dict[str, Any],
229
+ data_infos: list[DFMetaData],
230
+ custom_charts: list[ChartPlan],
231
+ auto_open: bool = True,
232
+ ) -> ValidateResults:
233
+ """Validate Vizro model configuration. Run ALWAYS when you have a complete dashboard configuration.
234
+
235
+ If successful, the tool will return the python code and, if it is a remote file, the py.cafe link to the chart.
236
+ The PyCafe link will be automatically opened in your default browser if auto_open is True.
237
+
238
+ Args:
239
+ dashboard_config: Either a JSON string or a dictionary representing a Vizro dashboard model configuration
240
+ data_infos: List of DFMetaData objects containing information about the data files
241
+ custom_charts: List of ChartPlan objects containing information about the custom charts in the dashboard
242
+ auto_open: Whether to automatically open the PyCafe link in a browser
243
+
244
+ Returns:
245
+ ValidationResults object with status and dashboard details
246
+ """
247
+ Vizro._reset()
248
+
249
+ try:
250
+ dashboard = vm.Dashboard.model_validate(
251
+ dashboard_config,
252
+ context={"allow_undefined_captured_callable": [custom_chart.chart_name for custom_chart in custom_charts]},
253
+ )
254
+ except ValidationError as e:
255
+ return ValidateResults(
256
+ valid=False,
257
+ message=f"""Validation Error: {e!s}. Fix the error and call this tool again.
258
+ Calling `get_model_json_schema` may help.""",
259
+ python_code="",
260
+ pycafe_url=None,
261
+ browser_opened=False,
262
+ )
263
+
264
+ else:
265
+ code_link = get_python_code_and_preview_link(dashboard, data_infos, custom_charts)
266
+
267
+ pycafe_url = code_link.pycafe_url if all(info.file_location_type == "remote" for info in data_infos) else None
268
+ browser_opened = False
269
+
270
+ if pycafe_url and auto_open:
271
+ try:
272
+ browser_opened = webbrowser.open(pycafe_url)
273
+ except Exception:
274
+ browser_opened = False
275
+
276
+ return ValidateResults(
277
+ valid=True,
278
+ message="""Configuration is valid for Dashboard! Do not forget to call this tool again after each iteration.
279
+ If you are creating an `app.py` file, you MUST use the code from the validation tool, do not modify it, watch out for
280
+ differences to previous `app.py`""",
281
+ python_code=code_link.python_code,
282
+ pycafe_url=pycafe_url,
283
+ browser_opened=browser_opened,
284
+ )
285
+
286
+ finally:
287
+ Vizro._reset()
288
+
289
+
292
290
  @mcp.prompt()
293
291
  def create_starter_dashboard():
294
292
  """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
293
+ return get_starter_dashboard_prompt()
308
294
 
309
295
 
310
296
  @mcp.prompt()
311
- def create_eda_dashboard(
312
- file_path_or_url: str,
297
+ def create_dashboard(
298
+ file_path_or_url: str = Field(description="The absolute path or URL to the data file you want to use."),
299
+ context: Optional[str] = Field(default=None, description="(Optional) Describe the dashboard you want to create."),
313
300
  ) -> str:
314
301
  """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
302
+ return get_dashboard_prompt(file_path_or_url, context)
332
303
 
333
304
 
334
305
  @mcp.tool()
@@ -336,7 +307,7 @@ def validate_chart_code(
336
307
  chart_config: ChartPlan,
337
308
  data_info: DFMetaData,
338
309
  auto_open: bool = True,
339
- ) -> ValidationResults:
310
+ ) -> ValidateResults:
340
311
  """Validate the chart code created by the user and optionally open the PyCafe link in a browser.
341
312
 
342
313
  Args:
@@ -352,7 +323,7 @@ def validate_chart_code(
352
323
  try:
353
324
  chart_plan_obj = ChartPlan.model_validate(chart_config)
354
325
  except ValidationError as e:
355
- return ValidationResults(
326
+ return ValidateResults(
356
327
  valid=False,
357
328
  message=f"Validation Error: {e!s}",
358
329
  python_code="",
@@ -372,7 +343,7 @@ def validate_chart_code(
372
343
  except Exception:
373
344
  browser_opened = False
374
345
 
375
- return ValidationResults(
346
+ return ValidateResults(
376
347
  valid=True,
377
348
  message="Chart only dashboard created successfully!",
378
349
  python_code=chart_plan_obj.get_chart_code(vizro=True),
@@ -386,15 +357,8 @@ def validate_chart_code(
386
357
 
387
358
  @mcp.prompt()
388
359
  def create_vizro_chart(
389
- chart_type: str,
390
- file_path_or_url: Optional[str] = None,
360
+ file_path_or_url: str = Field(description="The absolute path or URL to the data file you want to use."),
361
+ context: Optional[str] = Field(default=None, description="(Optional) Describe the chart you want to create."),
391
362
  ) -> str:
392
363
  """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
364
+ return get_chart_prompt(file_path_or_url, context)