Skip to content

Commit 2026b6f

Browse files
authored
Fix Claude Desktop dropping tools (#131)
* Fix tool output schemas for Claude Desktop compatibility Claude Desktop filters out tools with non-object outputSchema types. - get_models: return dict wrapping list instead of bare list - create_chart: annotate as dict (runtime still returns list in apps mode, FastMCP handles conversion regardless of annotation) * Fix Claude Desktop dropping tools due to outputSchema and anyOf Claude Desktop silently filters out tools that have outputSchema in the tools/list response (mcp==1.26.0 auto-generates these) or anyOf in inputSchema (from `T | None` type annotations). - Add structured_output=False to all tool decorators - Replace `list[str] | None = None` with `list[str] = []` - Replace `int | None = None` with `int = 0` (convert back via `or None`) - Replace `str | None = None` with `str = ""` for where/title params
1 parent e331f8d commit 2026b6f

2 files changed

Lines changed: 42 additions & 41 deletions

File tree

sidemantic/mcp_server.py

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ def _format_join_condition(model_name: str, rel, models: dict[str, Any]) -> str
152152
mcp = FastMCP("sidemantic")
153153

154154

155-
@mcp.tool()
156-
def get_models(model_names: list[str]) -> list[dict[str, Any]]:
155+
@mcp.tool(structured_output=False)
156+
def get_models(model_names: list[str]) -> dict[str, Any]:
157157
"""Get detailed information about one or more models.
158158
159159
Returns full definitions including all dimensions (with types, SQL, granularity),
@@ -317,18 +317,18 @@ def get_models(model_names: list[str]) -> list[dict[str, Any]]:
317317

318318
details.append(detail)
319319

320-
return details
320+
return {"models": details}
321321

322322

323-
@mcp.tool()
323+
@mcp.tool(structured_output=False)
324324
def run_query(
325-
dimensions: list[str] | None = None,
326-
metrics: list[str] | None = None,
327-
where: str | None = None,
328-
segments: list[str] | None = None,
329-
order_by: list[str] | None = None,
330-
limit: int | None = None,
331-
offset: int | None = None,
325+
dimensions: list[str] = [],
326+
metrics: list[str] = [],
327+
where: str = "",
328+
segments: list[str] = [],
329+
order_by: list[str] = [],
330+
limit: int = 0,
331+
offset: int = 0,
332332
ungrouped: bool = False,
333333
dry_run: bool = False,
334334
) -> dict[str, Any]:
@@ -368,8 +368,8 @@ def run_query(
368368
filters=[where] if where else None,
369369
segments=segments,
370370
order_by=order_by,
371-
limit=limit,
372-
offset=offset,
371+
limit=limit or None,
372+
offset=offset or None,
373373
ungrouped=ungrouped,
374374
)
375375

@@ -391,20 +391,20 @@ def run_query(
391391
}
392392

393393

394-
@mcp.tool(meta={"ui": {"resourceUri": "ui://sidemantic/chart"}})
394+
@mcp.tool(structured_output=False, meta={"ui": {"resourceUri": "ui://sidemantic/chart"}})
395395
def create_chart(
396-
dimensions: list[str] | None = None,
397-
metrics: list[str] | None = None,
398-
where: str | None = None,
399-
segments: list[str] | None = None,
400-
order_by: list[str] | None = None,
401-
limit: int | None = None,
402-
offset: int | None = None,
396+
dimensions: list[str] = [],
397+
metrics: list[str] = [],
398+
where: str = "",
399+
segments: list[str] = [],
400+
order_by: list[str] = [],
401+
limit: int = 0,
402+
offset: int = 0,
403403
chart_type: Literal["auto", "bar", "line", "area", "scatter", "point"] = "auto",
404-
title: str | None = None,
404+
title: str = "",
405405
width: int = 600,
406406
height: int = 400,
407-
) -> dict[str, Any] | list[Any]:
407+
) -> dict[str, Any]:
408408
"""Generate a chart from a semantic layer query, producing a Vega-Lite spec and PNG.
409409
410410
Query parameters work the same as run_query (model.field_name references,
@@ -449,8 +449,8 @@ def create_chart(
449449
filters=[where] if where else None,
450450
segments=segments,
451451
order_by=order_by,
452-
limit=limit,
453-
offset=offset,
452+
limit=limit or None,
453+
offset=offset or None,
454454
)
455455

456456
result = layer.adapter.execute(sql)
@@ -466,7 +466,7 @@ def create_chart(
466466
)
467467

468468
# Auto-generate title if not provided
469-
if title is None:
469+
if not title:
470470
title = _generate_chart_title(dimensions or [], metrics or [])
471471

472472
# Create chart with beautiful defaults
@@ -536,7 +536,7 @@ def _format_field_name(field: str) -> str:
536536
return field.replace("_", " ").title()
537537

538538

539-
@mcp.tool()
539+
@mcp.tool(structured_output=False)
540540
def run_sql(query: str) -> dict[str, Any]:
541541
"""Execute a SQL query rewritten through the semantic layer.
542542
@@ -578,10 +578,10 @@ def run_sql(query: str) -> dict[str, Any]:
578578
}
579579

580580

581-
@mcp.tool()
581+
@mcp.tool(structured_output=False)
582582
def validate_query(
583-
dimensions: list[str] | None = None,
584-
metrics: list[str] | None = None,
583+
dimensions: list[str] = [],
584+
metrics: list[str] = [],
585585
) -> dict[str, Any]:
586586
"""Validate dimension and metric references before running a query.
587587
@@ -614,7 +614,7 @@ def validate_query(
614614
}
615615

616616

617-
@mcp.tool()
617+
@mcp.tool(structured_output=False)
618618
def get_semantic_graph() -> dict[str, Any]:
619619
"""Discover the semantic layer: all models, relationships, and available fields.
620620

tests/test_mcp_server.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ def test_discover_models_via_semantic_graph(demo_layer):
116116

117117
def test_get_models(demo_layer):
118118
"""Test getting detailed model information."""
119-
models = get_models(["orders"])
119+
result = get_models(["orders"])
120+
models = result["models"]
120121

121122
assert len(models) == 1
122123
model = models[0]
@@ -145,15 +146,15 @@ def test_get_models(demo_layer):
145146

146147
def test_get_models_nonexistent(demo_layer):
147148
"""Test getting a model that doesn't exist."""
148-
models = get_models(["nonexistent"])
149-
assert len(models) == 0
149+
result = get_models(["nonexistent"])
150+
assert len(result["models"]) == 0
150151

151152

152153
def test_get_models_multiple(demo_layer):
153154
"""Test getting multiple models (only one exists)."""
154-
models = get_models(["orders", "nonexistent"])
155-
assert len(models) == 1
156-
assert models[0]["name"] == "orders"
155+
result = get_models(["orders", "nonexistent"])
156+
assert len(result["models"]) == 1
157+
assert result["models"][0]["name"] == "orders"
157158

158159

159160
def test_run_query_basic(demo_layer):
@@ -455,8 +456,8 @@ def test_validate_query_invalid_metric(demo_layer):
455456

456457
def test_segments_via_get_models(demo_layer):
457458
"""Test that segments are accessible via get_models (replaces list_segments)."""
458-
models = get_models(["orders"])
459-
model = models[0]
459+
result = get_models(["orders"])
460+
model = result["models"][0]
460461

461462
assert "segments" in model
462463
assert len(model["segments"]) == 2
@@ -489,8 +490,8 @@ def test_get_semantic_graph(demo_layer):
489490

490491
def test_get_models_enriched(demo_layer):
491492
"""Test that get_models returns enriched model data."""
492-
models = get_models(["orders"])
493-
model = models[0]
493+
result = get_models(["orders"])
494+
model = result["models"][0]
494495

495496
# Check new fields
496497
assert model["primary_key"] == "id"

0 commit comments

Comments
 (0)