Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,18 @@ jobs:
test -f dist/synapse-runtime.iife.global.js
test -f dist/synapse-ui.iife.global.js
- name: Vendored Python IIFE is fresh (fail on drift from the build)
run: diff dist/synapse-ui.iife.global.js python/synapse_ui/_assets/synapse-ui.iife.js
run: diff dist/synapse-ui.iife.global.js python/nimblebrain_synapse/_assets/synapse-ui.iife.js

python:
name: Python (nimblebrain-synapse)
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8
- run: uv sync
- run: uv run ruff check nimblebrain_synapse tests
- run: uv run ruff format --check nimblebrain_synapse tests
- run: uv run pytest -q
76 changes: 76 additions & 0 deletions .github/workflows/publish-python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Publish nimblebrain-synapse to PyPI

on:
push:
tags:
- 'nimblebrain-synapse-v*'

defaults:
run:
working-directory: python

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8
- run: uv sync
- run: uv run ruff check nimblebrain_synapse tests
- run: uv run ruff format --check nimblebrain_synapse tests
- run: uv run pytest -q

publish:
needs: verify
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: write # create the GitHub Release
id-token: write # PyPI trusted publishing (OIDC)
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8

- name: Verify tag matches package version
run: |
TAG_VERSION="${GITHUB_REF#refs/tags/nimblebrain-synapse-v}"
PKG_VERSION=$(python3 -c \
"import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) != pyproject version ($PKG_VERSION)"
exit 1
fi

- name: Build sdist + wheel
run: uv build

# Trusted publishing (OIDC) — no token. Requires a PyPI publisher registered
# for repo NimbleBrainInc/synapse, workflow publish-python.yml, environment pypi.
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: python/dist

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#nimblebrain-synapse-v}"
NOTES=$(awk -v ver="$VERSION" '
$0 ~ "^## \\[" ver "\\]" { capture=1; next }
capture && /^## \[/ { exit }
capture { print }
' CHANGELOG.md)
if [ -z "$(printf '%s' "$NOTES" | tr -d '[:space:]')" ]; then
echo "::error::No python/CHANGELOG.md section found for version $VERSION"
exit 1
fi
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release $TAG already exists; skipping."
exit 0
fi
printf '%s\n' "$NOTES" | gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "$TAG" \
--notes-file -
22 changes: 22 additions & 0 deletions python/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Changelog

All notable changes to `nimblebrain-synapse` (the Python package) are documented here.
It versions **independently** of the `@nimblebrain/synapse` npm package — the two
meet only on the wire protocol, not on a shared version number.

This project adheres to [Semantic Versioning](https://semver.org/).

## [0.1.0]

First published release to PyPI. The `SynapseUI` server descriptor previously
shipped only as source in this repo and vendored copies; it is now an installable
package so bundles depend on one source of truth instead of copying it.

### Added

- `SynapseUI` server descriptor: dual-MIME `ui://` registration (ChatGPT
`text/html+skybridge` + MCP Apps `text/html;profile=mcp-app`), tool/result
`_meta` in both dialects, widget CSP + domain, `<script>`-safe data embedding
(the XSS defense), and the quarantined `CallToolResult` render injection.
- Bundled client IIFE (`window.SynapseUI`) from `@nimblebrain/synapse` 0.12.0,
inlined so a component stays self-contained (CSP-safe, no CDN).
36 changes: 29 additions & 7 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
# synapse-ui (Python)
# nimblebrain-synapse (Python)

The **server half** of the Synapse cross-host UI framework. Pairs with the
`@nimblebrain/synapse` client (`connectUI` / `window.SynapseUI`).

```bash
pip install nimblebrain-synapse # or: uv add nimblebrain-synapse
```

One `SynapseUI` declaration wires a self-contained HTML component into every host
bridge a Synapse app renders in — **ChatGPT** (OpenAI Apps SDK) and **Claude**
(mcp-ui) — from a FastMCP server, replacing the per-app hand-rolled shim.
bridge a Synapse app renders in — **ChatGPT** (OpenAI Apps SDK), **Claude** (MCP
Apps), and the **NimbleBrain** runtime — from a FastMCP server, replacing the
per-app hand-rolled shim.

```python
from mcp.server.fastmcp import FastMCP
from synapse_ui import SynapseUI
from nimblebrain_synapse import SynapseUI

mcp = FastMCP("bassethound")

Expand Down Expand Up @@ -53,7 +58,24 @@ result-transform hook` note in `server.py`.

## Client SDK asset

`synapse_ui/_assets/synapse-ui.iife.js` is the vendored client IIFE
`nimblebrain_synapse/_assets/synapse-ui.iife.js` is the vendored client IIFE
(`window.SynapseUI`), regenerated from the JS build
(`dist/synapse-ui.iife.global.js`). Server and client ship together so their
versions stay coupled.
(`dist/synapse-ui.iife.global.js`) and inlined at register time so a component
is fully self-contained (CSP-safe, no CDN). CI fails on drift from the build.
`nimblebrain_synapse.__client_version__` records which `@nimblebrain/synapse` release the
bundled IIFE was built from.

## Versioning & compatibility

`nimblebrain-synapse` (PyPI) versions **independently** of `@nimblebrain/synapse` (npm).
They change for different reasons at different cadences — the server descriptor is
thin and stable; the JS client evolves with host adapters and theming — so they do
not share a version number. What they share is the **wire protocol**: the `ui://`
resource MIMEs, the `_meta` dialects, and the data-element contract. Both sides
target the same named protocol versions.

| `nimblebrain-synapse` | Bundled client (`@nimblebrain/synapse`) | Wire protocols |
|---|---|---|
| 0.1.0 | 0.12.0 | ext-apps `2026-01-26` · MCP Apps (SEP-1865) · OpenAI Apps SDK |

Releases publish on a `nimblebrain-synapse-v*` tag (distinct from the npm `v*` tags).
24 changes: 24 additions & 0 deletions python/nimblebrain_synapse/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""nimblebrain-synapse — the server (Python) half of the Synapse cross-host UI framework.

Pairs with the `@nimblebrain/synapse` client (`connectUI` / `window.SynapseUI`).
See `SynapseUI` in `nimblebrain_synapse.server`.
"""

from __future__ import annotations

from .server import (
DEFAULT_DATA_ELEMENT_ID,
MCPUI_MIME,
SKYBRIDGE_MIME,
SynapseUI,
)

__all__ = ["SynapseUI", "SKYBRIDGE_MIME", "MCPUI_MIME", "DEFAULT_DATA_ELEMENT_ID"]

__version__ = "0.1.0"

# The `@nimblebrain/synapse` npm release the vendored client IIFE
# (`_assets/synapse-ui.iife.js`) was built from. This package versions
# independently of the JS one (different cadence, different consumers); the two
# meet only on the wire protocol. Bump this whenever the IIFE is re-vendored.
__client_version__ = "0.12.0"
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@


def _load_bundled_sdk() -> str:
return (resources.files("synapse_ui") / "_assets" / _SDK_ASSET).read_text(encoding="utf-8")
return (resources.files("nimblebrain_synapse") / "_assets" / _SDK_ASSET).read_text(
encoding="utf-8"
)


class SynapseUI:
Expand Down
27 changes: 22 additions & 5 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
[project]
name = "synapse-ui"
name = "nimblebrain-synapse"
version = "0.1.0"
description = "Server (Python) half of the Synapse cross-host UI framework one ui:// component rendered in ChatGPT (OpenAI Apps SDK) and Claude (mcp-ui) from FastMCP."
description = "Server half of the Synapse cross-host UI framework: register one self-contained ui:// component from a FastMCP server and render it in ChatGPT (OpenAI Apps SDK), Claude (MCP Apps), and the NimbleBrain runtime."
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
authors = [{ name = "NimbleBrain Inc." }]
keywords = ["mcp", "fastmcp", "mcp-ui", "openai-apps", "synapse", "ui"]
keywords = ["mcp", "fastmcp", "mcp-apps", "openai-apps", "synapse", "ui", "widget"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Application Frameworks",
]
dependencies = [
"mcp>=1.12",
]

[project.urls]
Homepage = "https://github.com/NimbleBrainInc/synapse"
Repository = "https://github.com/NimbleBrainInc/synapse"
Issues = "https://github.com/NimbleBrainInc/synapse/issues"
Changelog = "https://github.com/NimbleBrainInc/synapse/blob/main/python/CHANGELOG.md"

[dependency-groups]
dev = [
"ruff>=0.9",
Expand All @@ -23,12 +40,12 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["synapse_ui"]
packages = ["nimblebrain_synapse"]

[tool.hatch.build.targets.wheel.force-include]
# The vendored client SDK IIFE ships as package data so server + client versions
# stay coupled. Regenerated from the JS build (dist/synapse-ui.iife.global.js).
"synapse_ui/_assets/synapse-ui.iife.js" = "synapse_ui/_assets/synapse-ui.iife.js"
"nimblebrain_synapse/_assets/synapse-ui.iife.js" = "nimblebrain_synapse/_assets/synapse-ui.iife.js"

[tool.ruff]
line-length = 100
Expand Down
18 changes: 0 additions & 18 deletions python/synapse_ui/__init__.py

This file was deleted.

8 changes: 3 additions & 5 deletions python/tests/test_synapse_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel

from synapse_ui import SynapseUI
from synapse_ui.server import DATA_MARKER, SDK_MARKER
from nimblebrain_synapse import SynapseUI
from nimblebrain_synapse.server import DATA_MARKER, SDK_MARKER

UI_URI = "ui://test/report"

Expand Down Expand Up @@ -188,9 +188,7 @@ def test_bind_skips_errors_and_empty_results():

def test_bind_respects_should_render_predicate():
ui = _ui()
ctr = types.CallToolResult(
content=[], structuredContent={"unrelated": True}, isError=False
)
ctr = types.CallToolResult(content=[], structuredContent={"unrelated": True}, isError=False)
out = ui._attach(types.ServerResult(ctr), lambda d: "domain" in d).root
# Predicate rejects → no injection.
assert out.meta is None
Expand Down
Loading