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.
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/PKG-INFO +44 -8
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/README.md +43 -7
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/model2data_studio/__init__.py +1 -1
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/model2data_studio/cli.py +2 -1
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/model2data_studio/mcp_server.py +69 -26
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/pyproject.toml +1 -1
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/server.json +9 -3
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/tests/test_building.py +32 -0
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/uv.lock +4 -4
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/.gitignore +0 -0
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/LICENSE +0 -0
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/model2data_studio/archive.py +0 -0
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/model2data_studio/building.py +0 -0
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/model2data_studio/client.py +0 -0
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/model2data_studio/export.py +0 -0
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/tests/test_cli.py +0 -0
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/tests/test_export.py +0 -0
- {model2data_studio-0.4.0 → model2data_studio-0.5.0}/tests/test_mcp_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: model2data-studio
|
|
3
|
-
Version: 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
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
| --- | --- |
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
401
|
-
"`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
@@ -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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|