model2data-studio 0.4.0__tar.gz → 0.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: model2data-studio
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Generate a model2data studio project's data from the command line, so CI can rebuild fixtures instead of committing them.
5
5
  Project-URL: Homepage, https://www.jbanalytica.com/model2data/
6
6
  Project-URL: Studio, https://studio.jbanalytica.com
@@ -52,7 +52,10 @@ uvx model2data-studio --help
52
52
 
53
53
  It needs an API key from a **Pro** account on the studio: see
54
54
  [pricing](https://www.jbanalytica.com/model2data/pricing/). The client is MIT
55
- licensed; the studio it talks to is not open source.
55
+ licensed; the studio it talks to is not open source. (To use the studio from
56
+ Claude or another MCP client without a key, see [the MCP
57
+ server](#the-mcp-server-generating-from-an-agent-instead-of-a-shell): the
58
+ studio's own endpoint needs only a sign-in.)
56
59
 
57
60
  ## Configuration
58
61
 
@@ -362,11 +365,44 @@ would compare against rows anchored on today and fail at the first midnight.
362
365
 
363
366
  ## The MCP server: generating from an agent instead of a shell
364
367
 
365
- The same account, the same key, the same API — for the editor rather than the
366
- terminal. The studio is where a schema is *designed*, with a diagram and a
367
- preview to design it against; the agent in your editor is where the data is
368
- wanted, and asking it for "two hundred believable orders from my webshop model"
369
- should not mean pasting DBML into a chat.
368
+ The same account, the same API — for an agent rather than the terminal. The
369
+ studio is where a schema is *designed*, with a diagram and a preview to design
370
+ it against; the agent in your editor or chat is where the data is wanted, and
371
+ asking it for "two hundred believable orders from my webshop model" should not
372
+ mean pasting DBML into a chat.
373
+
374
+ There are two ways to connect, with the same tools behind both.
375
+
376
+ ### One URL: the studio's own MCP endpoint
377
+
378
+ The studio serves these tools itself at `https://studio.jbanalytica.com/mcp`.
379
+ Add that URL to your MCP client and sign in to the studio in your browser when
380
+ it asks; there is no key to copy, nothing to install and no config file. It
381
+ works with a free account, within the free plan's limits.
382
+
383
+ - **Claude** (web, desktop and mobile): *Settings → Connectors → Add custom
384
+ connector*, paste `https://studio.jbanalytica.com/mcp`, then *Connect* and
385
+ approve the connection in the studio.
386
+ - **Claude Code**:
387
+
388
+ ```bash
389
+ claude mcp add --transport http model2data-studio https://studio.jbanalytica.com/mcp
390
+ ```
391
+
392
+ then run `/mcp` in Claude Code and pick the server to sign in.
393
+ - **Anything else** that speaks Streamable HTTP with OAuth: the same URL.
394
+
395
+ A connected app appears in the studio under *API keys* in the account menu,
396
+ marked *Connected app*; *Disconnect* there takes its access away at once. It can
397
+ read your projects, create them and save their models, metrics files and
398
+ dashboards, and generate previews and metric values. It cannot delete a project,
399
+ share one, or manage your keys. `write_data` is not offered there: a server on
400
+ the studio would write the files to the studio's disk, not yours.
401
+
402
+ ### Local: this package, with an API key
403
+
404
+ For CI, for a self-hosted studio, or for writing fixtures straight to disk with
405
+ `write_data`, run the server from this package with an API key (Pro):
370
406
 
371
407
  ```bash
372
408
  uvx --from 'model2data-studio[mcp]' model2data-studio mcp # the SDK is an extra, see below
@@ -398,7 +434,7 @@ and for anything that reads a JSON config (Claude Desktop, Cursor, Zed):
398
434
 
399
435
  `MODEL2DATA_STUDIO_URL` works here too, for a self-hosted studio.
400
436
 
401
- Nineteen tools:
437
+ Nineteen tools (eighteen at the studio's own endpoint, which has no `write_data`):
402
438
 
403
439
  | | |
404
440
  | --- | --- |
@@ -23,7 +23,10 @@ uvx model2data-studio --help
23
23
 
24
24
  It needs an API key from a **Pro** account on the studio: see
25
25
  [pricing](https://www.jbanalytica.com/model2data/pricing/). The client is MIT
26
- licensed; the studio it talks to is not open source.
26
+ licensed; the studio it talks to is not open source. (To use the studio from
27
+ Claude or another MCP client without a key, see [the MCP
28
+ server](#the-mcp-server-generating-from-an-agent-instead-of-a-shell): the
29
+ studio's own endpoint needs only a sign-in.)
27
30
 
28
31
  ## Configuration
29
32
 
@@ -333,11 +336,44 @@ would compare against rows anchored on today and fail at the first midnight.
333
336
 
334
337
  ## The MCP server: generating from an agent instead of a shell
335
338
 
336
- The same account, the same key, the same API — for the editor rather than the
337
- terminal. The studio is where a schema is *designed*, with a diagram and a
338
- preview to design it against; the agent in your editor is where the data is
339
- wanted, and asking it for "two hundred believable orders from my webshop model"
340
- should not mean pasting DBML into a chat.
339
+ The same account, the same API — for an agent rather than the terminal. The
340
+ studio is where a schema is *designed*, with a diagram and a preview to design
341
+ it against; the agent in your editor or chat is where the data is wanted, and
342
+ asking it for "two hundred believable orders from my webshop model" should not
343
+ mean pasting DBML into a chat.
344
+
345
+ There are two ways to connect, with the same tools behind both.
346
+
347
+ ### One URL: the studio's own MCP endpoint
348
+
349
+ The studio serves these tools itself at `https://studio.jbanalytica.com/mcp`.
350
+ Add that URL to your MCP client and sign in to the studio in your browser when
351
+ it asks; there is no key to copy, nothing to install and no config file. It
352
+ works with a free account, within the free plan's limits.
353
+
354
+ - **Claude** (web, desktop and mobile): *Settings → Connectors → Add custom
355
+ connector*, paste `https://studio.jbanalytica.com/mcp`, then *Connect* and
356
+ approve the connection in the studio.
357
+ - **Claude Code**:
358
+
359
+ ```bash
360
+ claude mcp add --transport http model2data-studio https://studio.jbanalytica.com/mcp
361
+ ```
362
+
363
+ then run `/mcp` in Claude Code and pick the server to sign in.
364
+ - **Anything else** that speaks Streamable HTTP with OAuth: the same URL.
365
+
366
+ A connected app appears in the studio under *API keys* in the account menu,
367
+ marked *Connected app*; *Disconnect* there takes its access away at once. It can
368
+ read your projects, create them and save their models, metrics files and
369
+ dashboards, and generate previews and metric values. It cannot delete a project,
370
+ share one, or manage your keys. `write_data` is not offered there: a server on
371
+ the studio would write the files to the studio's disk, not yours.
372
+
373
+ ### Local: this package, with an API key
374
+
375
+ For CI, for a self-hosted studio, or for writing fixtures straight to disk with
376
+ `write_data`, run the server from this package with an API key (Pro):
341
377
 
342
378
  ```bash
343
379
  uvx --from 'model2data-studio[mcp]' model2data-studio mcp # the SDK is an extra, see below
@@ -369,7 +405,7 @@ and for anything that reads a JSON config (Claude Desktop, Cursor, Zed):
369
405
 
370
406
  `MODEL2DATA_STUDIO_URL` works here too, for a self-hosted studio.
371
407
 
372
- Nineteen tools:
408
+ Nineteen tools (eighteen at the studio's own endpoint, which has no `write_data`):
373
409
 
374
410
  | | |
375
411
  | --- | --- |
@@ -12,6 +12,6 @@ the studio's API does all of that, and this only knows how to ask.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- __version__ = "0.4.0"
15
+ __version__ = "0.5.0"
16
16
 
17
17
  __all__ = ["__version__"]
@@ -969,7 +969,8 @@ def metrics_values(
969
969
  described = {metric.get("name"): metric for metric in listed}
970
970
  typer.echo(
971
971
  f"{found.name}: seed {resolved.seed}, dates anchored on {resolved.anchor:%Y-%m-%d}, "
972
- f"{resolved.body.get('rows')} rows a table"
972
+ f"{resolved.body.get('rows')} rows a table unless the model or project sets a "
973
+ "table's own count"
973
974
  )
974
975
  typer.echo(
975
976
  _table(
@@ -57,6 +57,11 @@ rows.
57
57
 
58
58
  Run it as `model2data-studio-mcp`, with MODEL2DATA_STUDIO_KEY in the
59
59
  environment; see the README for the client configuration.
60
+
61
+ The studio also serves these same tools itself, at `<studio>/mcp`, for clients
62
+ that connect with a URL and a browser sign-in instead of a key: it calls
63
+ `build` with a client factory of its own, and without `write_data`, which
64
+ writes to the disk of the machine the server runs on.
60
65
  """
61
66
 
62
67
  from __future__ import annotations
@@ -66,6 +71,7 @@ import functools
66
71
  import io
67
72
  import os
68
73
  import zipfile
74
+ from collections.abc import Callable
69
75
  from pathlib import Path
70
76
  from typing import Any
71
77
 
@@ -353,13 +359,33 @@ def preview(archive: bytes, *, rows: int = PREVIEW_ROWS, budget: int = PREVIEW_C
353
359
  return "\n\n".join(sections)
354
360
 
355
361
 
356
- def build() -> Any:
362
+ ClientFactory = Callable[..., StudioClient]
363
+
364
+
365
+ def build(
366
+ client_factory: ClientFactory | None = None,
367
+ *,
368
+ local_files: bool = True,
369
+ thread_limiter: Any = None,
370
+ ) -> Any:
357
371
  """The server, built on first use.
358
372
 
359
373
  A function rather than a module-level object so that importing this module
360
374
  -- which the tests do, to call the logic above directly -- does not require
361
375
  the MCP SDK to be installed. It is an optional dependency: this package is
362
376
  installed on CI runners that want the CLI and nothing else.
377
+
378
+ The arguments are for a server that is not this process's own (the
379
+ studio's remote endpoint); the defaults are the local server, unchanged.
380
+
381
+ - `client_factory(required=...)` makes each tool call's `StudioClient`;
382
+ by default `_client`, from the environment's key.
383
+ - `local_files=False` leaves out `write_data`: written by a server
384
+ somewhere else, the files would land on that server.
385
+ - `thread_limiter`, an `anyio.CapacityLimiter`, runs each tool on a worker
386
+ thread under that limiter instead of the default one. A tool that calls
387
+ back into the same process's API must not hold one of the threads that
388
+ API needs to answer it.
363
389
  """
364
390
  try:
365
391
  # `MCPServer` since mcp 2.0, where it was renamed from `FastMCP`.
@@ -372,6 +398,10 @@ def build() -> Any:
372
398
 
373
399
  from mcp.server.mcpserver.exceptions import ToolError
374
400
 
401
+ def make_client(*, required: bool = True) -> StudioClient:
402
+ # `_client` looked up per call, so a test patching it still reaches here.
403
+ return (client_factory or _client)(required=required)
404
+
375
405
  def reported(tool):
376
406
  """Let the studio's own sentence reach the model.
377
407
 
@@ -390,15 +420,26 @@ def build() -> Any:
390
420
  except StudioError as error:
391
421
  raise ToolError(str(error)) from error
392
422
 
393
- return wrapper
423
+ if thread_limiter is None:
424
+ return wrapper
425
+
426
+ import anyio.to_thread
427
+
428
+ @functools.wraps(tool)
429
+ async def threaded(*args: Any, **kwargs: Any) -> Any:
430
+ call = functools.partial(wrapper, *args, **kwargs)
431
+ return await anyio.to_thread.run_sync(call, limiter=thread_limiter)
432
+
433
+ return threaded
394
434
 
395
435
  server = MCPServer(
396
436
  "model2data-studio",
397
437
  instructions=(
398
438
  "Realistic fake data from a schema designed in model2data studio. "
399
439
  "`list_projects` first if no project id is at hand, `describe_project` to see what "
400
- "a project holds, `preview_data` to read rows, `write_data` to put them on disk, "
401
- "`metric_values` for the known answers a query over those rows must return. "
440
+ "a project holds, `preview_data` to read rows, "
441
+ + ("`write_data` to put them on disk, " if local_files else "")
442
+ + "`metric_values` for the known answers a query over those rows must return. "
402
443
  "Models, metrics and dashboards are files in model2data's open specs; each has a "
403
444
  "guide, a check and a save tool. " + RECIPE
404
445
  ),
@@ -412,7 +453,7 @@ def build() -> Any:
412
453
  Worth reading before asking for a lot of rows: the caps are per plan,
413
454
  and a refusal is a 402 rather than a smaller answer.
414
455
  """
415
- with _client() as client:
456
+ with make_client() as client:
416
457
  usage = client.get_usage()
417
458
  plan = client.get_plan() or {}
418
459
  lines = [f"Plan: {plan.get('name', usage.get('plan', 'unknown'))}"]
@@ -433,7 +474,7 @@ def build() -> Any:
433
474
  def list_projects() -> str:
434
475
  """The account's saved projects: the id to pass to the other tools,
435
476
  the name, and when it was last saved."""
436
- with _client() as client:
477
+ with make_client() as client:
437
478
  projects = client.list_projects()
438
479
  if not projects:
439
480
  return "This account has no saved projects yet."
@@ -455,7 +496,7 @@ def build() -> Any:
455
496
  (`{"min": 5.99, "max": 999.99}`, `{"null_rate": 0.4}`). Either way the
456
497
  hints are what decide the values a column comes out with.
457
498
  """
458
- with _client() as client:
499
+ with make_client() as client:
459
500
  saved = client.get_project(project_id)
460
501
  text = describe(saved)
461
502
  try:
@@ -477,7 +518,7 @@ def build() -> Any:
477
518
  the ones the model implies (a sum per measure column, a count per
478
519
  fact). `metric_values` gives their known values over generated data.
479
520
  """
480
- with _client() as client:
521
+ with make_client() as client:
481
522
  saved = client.get_project(project_id)
482
523
  checked = export.check_project(client, saved)
483
524
  return f"{saved.get('name') or 'Untitled'}\n\n{metrics_section(checked)}"
@@ -499,7 +540,7 @@ def build() -> Any:
499
540
  checked against them. Leave the arguments out and the project's own
500
541
  settings are used. Values are Pro, except on a bundled example.
501
542
  """
502
- with _client() as client:
543
+ with make_client() as client:
503
544
  saved = client.get_project(project_id)
504
545
  checked = export.check_project(client, saved)
505
546
  if any(i.get("severity") != "warning" for i in checked.get("issues") or []):
@@ -515,7 +556,8 @@ def build() -> Any:
515
556
  described = {metric.get("name"): metric for metric in checked.get("metrics") or []}
516
557
  head = (
517
558
  f"{saved.get('name') or 'Untitled'}: known values, seed {resolved.seed}, dates "
518
- f"anchored on {resolved.anchor:%Y-%m-%d}, {resolved.body.get('rows')} rows a table"
559
+ f"anchored on {resolved.anchor:%Y-%m-%d}, {resolved.body.get('rows')} rows a table "
560
+ "unless the model or project sets a table's own count"
519
561
  )
520
562
  if resolved.picked_seed:
521
563
  head += "\n(No saved seed, so one was picked. Pass it back as `seed` to reproduce.)"
@@ -544,7 +586,7 @@ def build() -> Any:
544
586
  overrides the project's. Leave them out and the project's own settings
545
587
  are used, which are the ones the studio previewed.
546
588
  """
547
- with _client() as client:
589
+ with make_client() as client:
548
590
  saved = client.get_project(project_id)
549
591
  resolved = export.resolve(
550
592
  saved,
@@ -562,8 +604,6 @@ def build() -> Any:
562
604
  head += "to generate these same rows again.)"
563
605
  return f"{head}\n\n{preview(archive)}"
564
606
 
565
- @server.tool()
566
- @reported
567
607
  def write_data(
568
608
  project_id: str,
569
609
  out_dir: str,
@@ -582,7 +622,7 @@ def build() -> Any:
582
622
  """
583
623
  if export_format not in {"csv", "dbt"}:
584
624
  raise StudioError(f'export_format is "csv" or "dbt", not {export_format!r}.')
585
- with _client() as client:
625
+ with make_client() as client:
586
626
  saved = client.get_project(project_id)
587
627
  resolved = export.resolve(
588
628
  saved,
@@ -600,6 +640,9 @@ def build() -> Any:
600
640
  lines.append(f"Seed {resolved.seed}, dates anchored on {resolved.anchor:%Y-%m-%d}.")
601
641
  return "\n".join(lines)
602
642
 
643
+ if local_files:
644
+ server.tool()(reported(write_data))
645
+
603
646
  @server.tool()
604
647
  @reported
605
648
  def get_model_guide() -> str:
@@ -608,7 +651,7 @@ def build() -> Any:
608
651
  with examples, then what the studio adds. Read it first when building a
609
652
  project: the model comes before metrics and dashboards. Needs no API key.
610
653
  """
611
- with _client(required=False) as client:
654
+ with make_client(required=False) as client:
612
655
  return client.model_guide()
613
656
 
614
657
  @server.tool()
@@ -621,7 +664,7 @@ def build() -> Any:
621
664
  `create_project` or `save_model`. A model without errors is summarised
622
665
  (tables, columns).
623
666
  """
624
- with _client(required=False) as client:
667
+ with make_client(required=False) as client:
625
668
  return checked_model_text(client.check_model(text))
626
669
 
627
670
  @server.tool()
@@ -637,7 +680,7 @@ def build() -> Any:
637
680
  `save_metrics`. Needs a key with save access that is not limited to one
638
681
  project; the plan's project limit applies.
639
682
  """
640
- with _client() as client:
683
+ with make_client() as client:
641
684
  try:
642
685
  created = building.create_project(client, name, model_text, metrics_text)
643
686
  except building.Rejected as error:
@@ -672,7 +715,7 @@ def build() -> Any:
672
715
  metrics file still reads against the new model. Needs a key with save
673
716
  access.
674
717
  """
675
- with _client() as client:
718
+ with make_client() as client:
676
719
  try:
677
720
  saved = building.save_model(client, project_id, text)
678
721
  except building.Rejected as error:
@@ -703,7 +746,7 @@ def build() -> Any:
703
746
  with examples, then what the studio adds. Read it after the model is
704
747
  written and before writing metrics. Needs no API key.
705
748
  """
706
- with _client(required=False) as client:
749
+ with make_client(required=False) as client:
707
750
  return client.metrics_guide()
708
751
 
709
752
  @server.tool()
@@ -720,7 +763,7 @@ def build() -> Any:
720
763
  """
721
764
  if (project_id is None) == (model_text is None):
722
765
  raise StudioError("Pass project_id or model_text, one of the two.")
723
- with _client(required=project_id is not None) as client:
766
+ with make_client(required=project_id is not None) as client:
724
767
  checked = client.check_metrics(model_text, text, project_id=project_id)
725
768
  return checked_metrics_text(checked)
726
769
 
@@ -734,7 +777,7 @@ def build() -> Any:
734
777
  and removed. Next: `metric_values` to see their numbers, then the
735
778
  dashboard tools. Needs a key with save access.
736
779
  """
737
- with _client() as client:
780
+ with make_client() as client:
738
781
  try:
739
782
  saved = building.save_metrics(client, project_id, text)
740
783
  except building.Rejected as error:
@@ -756,7 +799,7 @@ def build() -> Any:
756
799
  A dashboard is a file, `<name>.dashboard.yml`; `get_dashboard` gives one
757
800
  file's text and `get_dashboard_guide` says how to write one.
758
801
  """
759
- with _client() as client:
802
+ with make_client() as client:
760
803
  dashboards = client.list_dashboards(project_id)
761
804
  where = client.project_url(project_id)
762
805
  if not dashboards:
@@ -774,7 +817,7 @@ def build() -> Any:
774
817
  """One dashboard file's text, exactly as stored: edit it and pass it back
775
818
  to `save_dashboard` to change the dashboard. `name` is the file name
776
819
  without `.dashboard.yml`."""
777
- with _client() as client:
820
+ with make_client() as client:
778
821
  return str(client.get_dashboard(project_id, name).get("text") or "")
779
822
 
780
823
  @server.tool()
@@ -788,7 +831,7 @@ def build() -> Any:
788
831
  With `project_id`, it ends with what that project offers a dashboard: its
789
832
  model name, metrics (with labels) and `table.column` dimensions.
790
833
  """
791
- with _client(required=project_id is not None) as client:
834
+ with make_client(required=project_id is not None) as client:
792
835
  return client.dashboard_guide(project_id)
793
836
 
794
837
  @server.tool()
@@ -801,7 +844,7 @@ def build() -> Any:
801
844
  `project_id`, names the project does not have come back as warnings,
802
845
  which do not stop a save.
803
846
  """
804
- with _client(required=project_id is not None) as client:
847
+ with make_client(required=project_id is not None) as client:
805
848
  checked = client.check_dashboard(text, project_id)
806
849
  issues = dashboard_issues(checked)
807
850
  if checked.get("errors") or checked.get("ok") is False:
@@ -822,7 +865,7 @@ def build() -> Any:
822
865
  the same. The last step of building a project: give the user the
823
866
  studio URL (and share link) it answers with. Needs a key with save access.
824
867
  """
825
- with _client() as client:
868
+ with make_client() as client:
826
869
  checked = client.check_dashboard(text, project_id)
827
870
  issues = dashboard_issues(checked)
828
871
  if checked.get("errors") or checked.get("ok") is False:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "model2data-studio"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Generate a model2data studio project's data from the command line, so CI can rebuild fixtures instead of committing them."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -2,15 +2,15 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.JB-Analytica/model2data-studio",
4
4
  "title": "model2data studio",
5
- "description": "Realistic fake data from a schema designed in model2data studio: preview rows, write seeds.",
5
+ "description": "Design a data model, metrics and dashboards; generate realistic data and check known answers.",
6
6
  "websiteUrl": "https://www.jbanalytica.com/model2data/",
7
- "version": "0.4.0",
7
+ "version": "0.5.0",
8
8
  "packages": [
9
9
  {
10
10
  "registryType": "pypi",
11
11
  "registryBaseUrl": "https://pypi.org",
12
12
  "identifier": "model2data-studio",
13
- "version": "0.4.0",
13
+ "version": "0.5.0",
14
14
  "runtimeHint": "uvx",
15
15
  "runtimeArguments": [
16
16
  {
@@ -46,5 +46,11 @@
46
46
  }
47
47
  ]
48
48
  }
49
+ ],
50
+ "remotes": [
51
+ {
52
+ "type": "streamable-http",
53
+ "url": "https://studio.jbanalytica.com/mcp"
54
+ }
49
55
  ]
50
56
  }
@@ -601,3 +601,35 @@ class TestCli:
601
601
 
602
602
  assert result.exit_code == 0, result.output
603
603
  assert json.loads(fake.sent[0].content) == {"metrics": METRICS, "projectId": EXISTING["id"]}
604
+
605
+
606
+ @pytest.mark.anyio
607
+ class TestAServerBuiltForSomewhereElse:
608
+ """What the studio's own endpoint builds: its own clients, no `write_data`,
609
+ tools on a limiter of their own."""
610
+
611
+ async def test_it_uses_the_factory_and_leaves_out_write_data(self, monkeypatch):
612
+ import anyio
613
+
614
+ fake = FakeStudio()
615
+ made: list[bool] = []
616
+
617
+ def factory(*, required: bool = True) -> StudioClient:
618
+ made.append(required)
619
+ return _client(fake)
620
+
621
+ monkeypatch.delenv(mcp_server.KEY_ENV, raising=False)
622
+ server = mcp_server.build(
623
+ factory, local_files=False, thread_limiter=anyio.CapacityLimiter(1)
624
+ )
625
+
626
+ names = {tool.name for tool in await server.list_tools()}
627
+ assert "write_data" not in names
628
+ assert "create_project" in names
629
+ assert await call(server, "get_model_guide") == "# The model spec\n"
630
+ assert made == [False]
631
+
632
+ async def test_the_local_default_is_unchanged(self):
633
+ names = {tool.name for tool in await mcp_server.build().list_tools()}
634
+ assert "write_data" in names
635
+ assert len(names) == 19
@@ -248,7 +248,7 @@ name = "exceptiongroup"
248
248
  version = "1.3.1"
249
249
  source = { registry = "https://pypi.org/simple" }
250
250
  dependencies = [
251
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
251
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
252
252
  ]
253
253
  sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
254
254
  wheels = [
@@ -438,7 +438,7 @@ wheels = [
438
438
 
439
439
  [[package]]
440
440
  name = "model2data-studio"
441
- version = "0.4.0"
441
+ version = "0.5.0"
442
442
  source = { editable = "." }
443
443
  dependencies = [
444
444
  { name = "httpx" },
@@ -1159,8 +1159,8 @@ name = "uvicorn"
1159
1159
  version = "0.53.0"
1160
1160
  source = { registry = "https://pypi.org/simple" }
1161
1161
  dependencies = [
1162
- { name = "click" },
1163
- { name = "h11" },
1162
+ { name = "click", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" },
1163
+ { name = "h11", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" },
1164
1164
  { name = "typing-extensions", marker = "python_full_version < '3.11'" },
1165
1165
  ]
1166
1166
  sdist = { url = "https://files.pythonhosted.org/packages/5d/ad/04bbb797c84fc1f26cb171f7394716f4865ffb8d8c5e1eef42565c2dfa6b/uvicorn-0.53.0.tar.gz", hash = "sha256:a9356f0cb89b3b8621529c5d5eebd69bfe154f4c3f68b4cf2de47e45fa855c2e", size = 110881, upload-time = "2026-09-14T07:44:23.815Z" }