Skip to content

Commit cfc34cd

Browse files
authored
Vendor mcp-ui-server to fix MIME type and unblock PyPI publishing (#130)
PyPI rejects git deps in metadata. The PyPI v1.0.0 of mcp-ui-server has wrong MIME type (text/html instead of text/html;profile=mcp-app). Vendor the ~80 lines we need from the git source into sidemantic/apps/_mcp_ui.py with correct MIME type. Remove external dep.
1 parent 89acbe0 commit cfc34cd

6 files changed

Lines changed: 90 additions & 34 deletions

File tree

pyproject.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ mcp = [
4444
]
4545
apps = [
4646
"mcp[cli]>=1.25.0,<2",
47-
"mcp-ui-server @ git+https://github.com/MCP-UI-Org/mcp-ui.git#subdirectory=sdks/python/server", # PyPI v1.0.0 has wrong MIME type; switch to PyPI pin when they release
4847
]
4948
charts = [
5049
"altair>=5.0.0",
@@ -119,9 +118,6 @@ full = [
119118
requires = ["hatchling"]
120119
build-backend = "hatchling.build"
121120

122-
[tool.hatch.metadata]
123-
allow-direct-references = true
124-
125121
[tool.hatch.build.targets.sdist]
126122
exclude = [
127123
"/.claude",

sidemantic/apps/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""MCP Apps integration for sidemantic.
22
3-
Uses mcp-ui-server to create vendor-neutral UI resources that render
3+
Creates vendor-neutral UI resources (MCP Apps standard) that render
44
interactive charts in any MCP Apps-compatible host.
55
"""
66

@@ -45,7 +45,7 @@ def create_chart_resource(vega_spec: dict[str, Any]):
4545
Returns:
4646
UIResource (EmbeddedResource) for MCP Apps-compatible hosts.
4747
"""
48-
from mcp_ui_server import create_ui_resource
48+
from sidemantic.apps._mcp_ui import create_ui_resource
4949

5050
html = build_chart_html(vega_spec)
5151
return create_ui_resource(

sidemantic/apps/_mcp_ui.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Vendored subset of mcp-ui-server (https://github.com/MCP-UI-Org/mcp-ui).
2+
3+
Provides create_ui_resource() for building MCP Apps-compatible UI resources.
4+
Vendored because PyPI v1.0.0 has incorrect MIME type (text/html instead of
5+
text/html;profile=mcp-app) and the fix hasn't been published yet.
6+
7+
License: Apache-2.0 (MCP UI Contributors)
8+
"""
9+
10+
import base64
11+
from typing import Any, Literal
12+
13+
from mcp.types import BlobResourceContents, EmbeddedResource, TextResourceContents
14+
from pydantic import AnyUrl, BaseModel
15+
16+
# MIME type for MCP Apps resources
17+
RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"
18+
19+
20+
class UIResource(EmbeddedResource):
21+
"""A UI resource that can be included in tool results."""
22+
23+
def __init__(self, resource: TextResourceContents | BlobResourceContents, **kwargs: Any):
24+
super().__init__(type="resource", resource=resource, **kwargs)
25+
26+
27+
class RawHtmlPayload(BaseModel):
28+
type: Literal["rawHtml"]
29+
htmlString: str # noqa: N815
30+
31+
32+
class ExternalUrlPayload(BaseModel):
33+
type: Literal["externalUrl"]
34+
iframeUrl: str # noqa: N815
35+
36+
37+
class CreateUIResourceOptions(BaseModel):
38+
uri: str
39+
content: RawHtmlPayload | ExternalUrlPayload
40+
encoding: Literal["text", "blob"]
41+
42+
43+
def create_ui_resource(options_dict: dict[str, Any]) -> UIResource:
44+
"""Create a UIResource for inclusion in MCP tool results.
45+
46+
Args:
47+
options_dict: Configuration with keys:
48+
- uri: Resource identifier starting with 'ui://'
49+
- content: {"type": "rawHtml", "htmlString": "..."} or {"type": "externalUrl", "iframeUrl": "..."}
50+
- encoding: "text" or "blob"
51+
52+
Returns:
53+
UIResource (EmbeddedResource subclass) with correct MCP Apps MIME type.
54+
"""
55+
options = CreateUIResourceOptions.model_validate(options_dict)
56+
57+
if not options.uri.startswith("ui://"):
58+
raise ValueError(f"URI must start with 'ui://' but got: {options.uri}")
59+
60+
content = options.content
61+
if isinstance(content, RawHtmlPayload):
62+
content_string = content.htmlString
63+
elif isinstance(content, ExternalUrlPayload):
64+
content_string = content.iframeUrl
65+
else:
66+
raise ValueError(f"Invalid content type: {content.type}")
67+
68+
if options.encoding == "text":
69+
resource: TextResourceContents | BlobResourceContents = TextResourceContents(
70+
uri=AnyUrl(options.uri),
71+
mimeType=RESOURCE_MIME_TYPE,
72+
text=content_string,
73+
)
74+
elif options.encoding == "blob":
75+
resource = BlobResourceContents(
76+
uri=AnyUrl(options.uri),
77+
mimeType=RESOURCE_MIME_TYPE,
78+
blob=base64.b64encode(content_string.encode("utf-8")).decode("ascii"),
79+
)
80+
else:
81+
raise ValueError(f"Invalid encoding: {options.encoding}")
82+
83+
return UIResource(resource=resource)

sidemantic/cli.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -330,19 +330,10 @@ def mcp_serve(
330330

331331
# Enable apps mode if requested
332332
if apps:
333-
try:
334-
import mcp_ui_server # noqa: F401
335-
336-
import sidemantic.mcp_server as _mcp_mod
333+
import sidemantic.mcp_server as _mcp_mod
337334

338-
_mcp_mod._apps_enabled = True
339-
typer.echo("Interactive UI widgets enabled", err=True)
340-
except ImportError:
341-
typer.echo(
342-
"Error: mcp-ui-server not installed. Install with: uv add mcp-ui-server",
343-
err=True,
344-
)
345-
raise typer.Exit(1)
335+
_mcp_mod._apps_enabled = True
336+
typer.echo("Interactive UI widgets enabled", err=True)
346337

347338
# Determine transport
348339
if http or apps:

tests/test_mcp_apps.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
"""Tests for MCP Apps (mcp-ui-server) integration."""
1+
"""Tests for MCP Apps UI integration."""
22

33
import pytest
44

5-
pytest.importorskip("mcp_ui_server") # Skip if apps extra not installed
6-
pytest.importorskip("mcp")
5+
pytest.importorskip("mcp") # Skip if mcp extra not installed
76

87
from sidemantic.apps import build_chart_html, create_chart_resource
98
from sidemantic.mcp_server import create_chart, initialize_layer

uv.lock

Lines changed: 0 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)