Skip to content

Commit 26ada39

Browse files
deeleeramonedependabot[bot]claude
authored
Cowork plugin + .mcpb desktop extension + release automation (#23)
* build(deps): update mkdocs-literate-nav requirement in /pywry/docs (#22) Updates the requirements on [mkdocs-literate-nav](https://github.com/oprypin/mkdocs-literate-nav) to permit the latest version. - [Release notes](https://github.com/oprypin/mkdocs-literate-nav/releases) - [Commits](oprypin/mkdocs-literate-nav@v0.6.0...v0.6.3) --- updated-dependencies: - dependency-name: mkdocs-literate-nav dependency-version: 0.6.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): update griffe requirement in /pywry/docs (#21) Updates the requirements on [griffe](https://github.com/mkdocstrings/griffe) to permit the latest version. - [Release notes](https://github.com/mkdocstrings/griffe/releases) - [Changelog](https://github.com/mkdocstrings/griffe/blob/main/CHANGELOG.md) - [Commits](mkdocstrings/griffe@1.0.0...2.0.2) --- updated-dependencies: - dependency-name: griffe dependency-version: 2.0.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): update mkdocs-section-index requirement in /pywry/docs (#20) Updates the requirements on [mkdocs-section-index](https://github.com/oprypin/mkdocs-section-index) to permit the latest version. - [Release notes](https://github.com/oprypin/mkdocs-section-index/releases) - [Commits](oprypin/mkdocs-section-index@v0.3.0...v0.3.12) --- updated-dependencies: - dependency-name: mkdocs-section-index dependency-version: 0.3.12 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): update mkdocstrings requirement in /pywry/docs (#18) Updates the requirements on [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) to permit the latest version. - [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases) - [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md) - [Commits](mkdocstrings/mkdocstrings@0.26.0...1.0.4) --- updated-dependencies: - dependency-name: mkdocstrings dependency-version: 1.0.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): update mkdocs-gen-files requirement in /pywry/docs (#19) Updates the requirements on [mkdocs-gen-files](https://github.com/oprypin/mkdocs-gen-files) to permit the latest version. - [Release notes](https://github.com/oprypin/mkdocs-gen-files/releases) - [Commits](oprypin/mkdocs-gen-files@v0.5.0...v0.6.1) --- updated-dependencies: - dependency-name: mkdocs-gen-files dependency-version: 0.6.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com> * Add Cowork plugin variant, .mcpb extension, and release workflow Builds two new artifacts from claude/plugins/pywry/: a Cowork .plugin (stripped of .mcp.json + hooks/ for the hosted sandbox) and a Claude Desktop .mcpb bundle whose uv runtime resolves pywry[mcp] from PyPI. A push-to-main workflow tags claude-pywry-v<version> and uploads both artifacts; plugin-manifest.yml gains a sync check for the .mcpb manifest version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: deeleeramone <deeleeramone@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d7f5eb9 commit 26ada39

12 files changed

Lines changed: 492 additions & 28 deletions

File tree

.github/workflows/plugin-manifest.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,34 @@ jobs:
9393
except json.JSONDecodeError as e:
9494
failures.append(f"{name}: {hooks} invalid JSON: {e}")
9595
96+
# Desktop extension (.mcpb) is keyed to the pywry plugin specifically
97+
desktop_manifest = repo / "claude" / "desktop-extension" / "manifest.json"
98+
pywry_entry = next((p for p in plugins if p.get("name") == "pywry"), None)
99+
if pywry_entry is not None and desktop_manifest.exists():
100+
try:
101+
dm = json.loads(desktop_manifest.read_text(encoding="utf-8"))
102+
except json.JSONDecodeError as e:
103+
failures.append(f"{desktop_manifest}: invalid JSON: {e}")
104+
else:
105+
if dm.get("version") != pywry_entry.get("version"):
106+
failures.append(
107+
f"desktop-extension/manifest.json version "
108+
f"{dm.get('version')!r} != pywry plugin "
109+
f"{pywry_entry.get('version')!r}"
110+
)
111+
if dm.get("name") != "pywry":
112+
failures.append(
113+
f"desktop-extension/manifest.json name "
114+
f"{dm.get('name')!r} != 'pywry'"
115+
)
116+
elif pywry_entry is not None and not desktop_manifest.exists():
117+
failures.append(f"missing {desktop_manifest}")
118+
96119
if failures:
97120
for f in failures:
98121
print(f"::error::{f}")
99122
sys.exit(1)
100-
print(f"ok — {len(plugins)} plugin(s) validated")
123+
print(f"ok — {len(plugins)} plugin(s) validated, .mcpb manifest in sync")
101124
PY
102125
103126
- name: Check CHANGELOG was updated when plugin.json version changes
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
name: Release Claude artifacts
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
concurrency:
9+
group: release-claude-artifacts
10+
cancel-in-progress: false
11+
12+
permissions:
13+
contents: write
14+
15+
jobs:
16+
release:
17+
name: Tag and release on version bump
18+
runs-on: ubuntu-24.04
19+
steps:
20+
- uses: actions/checkout@v5
21+
22+
- name: Read plugin version
23+
id: version
24+
shell: bash
25+
run: |
26+
set -euo pipefail
27+
VERSION=$(jq -r .version claude/plugins/pywry/.claude-plugin/plugin.json)
28+
echo "value=$VERSION" >> "$GITHUB_OUTPUT"
29+
echo "tag=claude-pywry-v$VERSION" >> "$GITHUB_OUTPUT"
30+
echo "Plugin version: $VERSION"
31+
32+
- name: Skip if tag already exists
33+
id: tag_check
34+
env:
35+
TAG: ${{ steps.version.outputs.tag }}
36+
shell: bash
37+
run: |
38+
set -euo pipefail
39+
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "refs/tags/$TAG"; then
40+
echo "Tag $TAG already published — nothing to release."
41+
echo "exists=true" >> "$GITHUB_OUTPUT"
42+
else
43+
echo "Tag $TAG missing — will build and release."
44+
echo "exists=false" >> "$GITHUB_OUTPUT"
45+
fi
46+
47+
- name: Set up Python
48+
if: steps.tag_check.outputs.exists == 'false'
49+
uses: actions/setup-python@v6
50+
with:
51+
python-version: '3.12'
52+
53+
- name: Build distributions (enforces version sync across all three manifests)
54+
if: steps.tag_check.outputs.exists == 'false'
55+
run: python claude/scripts/build_distributions.py
56+
57+
- name: Extract changelog section
58+
id: notes
59+
if: steps.tag_check.outputs.exists == 'false'
60+
env:
61+
VERSION: ${{ steps.version.outputs.value }}
62+
shell: bash
63+
run: |
64+
set -euo pipefail
65+
NOTES_FILE="$RUNNER_TEMP/release-notes.md"
66+
python3 - "$VERSION" > "$NOTES_FILE" <<'PY'
67+
import re
68+
import sys
69+
70+
version = sys.argv[1]
71+
text = open("claude/plugins/pywry/CHANGELOG.md", encoding="utf-8").read()
72+
# Match either "## 1.2.3" or "## [1.2.3]" headings
73+
pattern = re.compile(
74+
rf"^## \[?{re.escape(version)}\]?[^\n]*\n(.*?)(?=^## |\Z)",
75+
re.MULTILINE | re.DOTALL,
76+
)
77+
m = pattern.search(text)
78+
if m:
79+
print(m.group(1).strip())
80+
else:
81+
print("See [CHANGELOG.md](claude/plugins/pywry/CHANGELOG.md) for details.")
82+
PY
83+
echo "file=$NOTES_FILE" >> "$GITHUB_OUTPUT"
84+
85+
- name: Configure git identity
86+
if: steps.tag_check.outputs.exists == 'false'
87+
shell: bash
88+
run: |
89+
git config user.name "github-actions[bot]"
90+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
91+
92+
- name: Tag and create GitHub release
93+
if: steps.tag_check.outputs.exists == 'false'
94+
env:
95+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96+
TAG: ${{ steps.version.outputs.tag }}
97+
VERSION: ${{ steps.version.outputs.value }}
98+
NOTES_FILE: ${{ steps.notes.outputs.file }}
99+
shell: bash
100+
run: |
101+
set -euo pipefail
102+
git tag -a "$TAG" -m "pywry plugin v$VERSION" "$GITHUB_SHA"
103+
git push origin "$TAG"
104+
gh release create "$TAG" \
105+
--title "pywry plugin v$VERSION" \
106+
--notes-file "$NOTES_FILE" \
107+
claude/dist/pywry-cowork.plugin \
108+
claude/dist/pywry.mcpb

claude/CONTRIBUTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ summary below:
4848
`Release claude/plugins/pywry v0.2.0`.
4949
5. Tag:
5050
```bash
51-
git tag -a plugin-pywry-v0.2.0 -m "pywry plugin v0.2.0"
52-
git push origin plugin-pywry-v0.2.0
51+
git tag -a claude-pywry-v0.2.0 -m "pywry plugin v0.2.0"
52+
git push origin claude-pywry-v0.2.0
5353
```
54-
6. Users pin with `/plugin install pywry@pywry --version plugin-pywry-v0.2.0`
54+
6. Users pin with `/plugin install pywry@pywry --version claude-pywry-v0.2.0`
5555
to avoid tracking `main`.
5656

5757
## Adding a new plugin

claude/README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ claude/
1111
├── .claude-plugin/
1212
│ └── marketplace.json # Single marketplace, lists every plugin below
1313
├── plugins/
14-
│ └── pywry/ # The canonical PyWry plugin
14+
│ └── pywry/ # The canonical PyWry plugin (Claude Code)
1515
│ ├── .claude-plugin/plugin.json
1616
│ ├── .mcp.json # Declares the `pywry` MCP server
1717
│ ├── agents/ # `pywry-builder` subagent
@@ -20,15 +20,50 @@ claude/
2020
│ ├── skills/ # pywry-orientation skill
2121
│ ├── CHANGELOG.md
2222
│ └── README.md
23+
├── desktop-extension/ # Claude Desktop MCP Bundle (.mcpb) source
24+
│ ├── manifest.json # MCPB manifest (different schema from plugin.json)
25+
│ ├── pyproject.toml # uv-runtime dependency spec → pywry[mcp]
26+
│ ├── src/server.py # Re-enters pywry.mcp.__main__:main
27+
│ └── README.md
28+
├── scripts/
29+
│ └── build_distributions.py # Builds dist/pywry-cowork.plugin and dist/pywry.mcpb
30+
├── dist/ # Build output (gitignored)
2331
├── CONTRIBUTING.md # How to add / maintain plugins here
2432
└── README.md # (this file)
2533
```
2634

35+
`claude/plugins/pywry/` is the single source of truth. The Cowork
36+
`.plugin` is a build-time transform of it (with `.mcp.json` and
37+
`hooks/` stripped); both are produced by
38+
[`scripts/build_distributions.py`](scripts/build_distributions.py).
39+
2740
Adding a sibling plugin later is a file-system operation: create
2841
`claude/plugins/<name>/`, add a `plugin.json`, and append an entry to
2942
`claude/.claude-plugin/marketplace.json`.
3043

31-
## Installing the plugin
44+
## Distribution variants
45+
46+
PyWry ships into three Claude surfaces from this directory:
47+
48+
| Variant | Artifact | Where it lives | Install path |
49+
|---|---|---|---|
50+
| Claude Code plugin | `claude/plugins/pywry/` (directory) | GitHub marketplace, PyPI-bundled wheel, local worktree | `/plugin marketplace add deeleeramone/PyWry --path claude/.claude-plugin/marketplace.json` then `/plugin install pywry@pywry` |
51+
| Cowork plugin | `claude/dist/pywry-cowork.plugin` (zip) | GitHub release asset; org marketplace upload | Cowork → Customize → Plugins → Upload plugin |
52+
| Claude Desktop extension | `claude/dist/pywry.mcpb` (zip) | GitHub release asset | Open the `.mcpb` file in Claude Desktop |
53+
54+
Build the two zipped artifacts with:
55+
56+
```bash
57+
python claude/scripts/build_distributions.py
58+
```
59+
60+
The Cowork variant intentionally drops `.mcp.json` and `hooks/`
61+
because Cowork's hosted sandbox cannot launch the local `pywry` CLI;
62+
skills, slash commands, and the `pywry-builder` subagent still work.
63+
The `.mcpb` ships only the MCP server (Claude Desktop has no concept
64+
of slash commands or subagents).
65+
66+
## Installing the Claude Code plugin
3267

3368
### From GitHub (primary path)
3469

claude/desktop-extension/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# pywry — Claude Desktop extension (`.mcpb`)
2+
3+
Native [Claude Desktop](https://claude.ai/download) integration for
4+
[PyWry](https://github.com/deeleeramone/PyWry). Same 66+ MCP tools as
5+
the Claude Code plugin, packaged as an [MCP Bundle](https://github.com/anthropics/dxt)
6+
so Claude Desktop installs and runs it without any manual `pip` step.
7+
8+
## Install
9+
10+
1. Grab `pywry.mcpb` from the latest GitHub release (or run
11+
`python scripts/build_distributions.py` from the repo root to build
12+
it locally).
13+
2. Open the file. Claude Desktop will show an install dialog —
14+
confirm to add the extension.
15+
3. The `uv` runtime resolves `pywry[mcp]>=2.0.0` from PyPI on first
16+
launch. Subsequent launches reuse the cached environment.
17+
18+
## Verify
19+
20+
Open a Claude Desktop conversation and ask: *"List the pywry MCP
21+
tools."* Claude should enumerate the `create_widget`, `show_plotly`,
22+
`show_dataframe`, `show_tvchart`, `tvchart_*`, and `create_chat_widget`
23+
families.
24+
25+
## Differences from the Claude Code plugin
26+
27+
This extension ships the **MCP server only**. Slash commands
28+
(`/pywry:doctor`, `/pywry:scaffold`, `/pywry:examples`), the
29+
`pywry-builder` subagent, the `pywry-orientation` skill, and the
30+
`ruff format` PostToolUse hook are Claude Code surfaces and are not
31+
expressible in the `.mcpb` format — install [the Claude Code plugin](../plugins/pywry/README.md)
32+
if you want those.
33+
34+
## License
35+
36+
Apache-2.0, same as PyWry.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"manifest_version": "0.4",
3+
"name": "pywry",
4+
"display_name": "PyWry",
5+
"version": "0.1.0",
6+
"description": "Native Claude Desktop integration for PyWry — MCP tools for generating and rendering HTML components, chat artifacts, and building native, web, or Jupyter applications with live preview. Built-in support for AgGrid, Plotly, TradingView, and more.",
7+
"long_description": "PyWry is a cross-platform rendering engine and desktop UI toolkit for Python. This Desktop Extension bundles the PyWry MCP server (66+ tools) so Claude Desktop can scaffold widgets, render Plotly charts, drive AG Grid tables, build TradingView lightweight charts, and produce streaming chat artifacts — all delivered as embedded HTML resources viewable directly in the artifact pane. The `uv` runtime resolves `pywry[mcp]` from PyPI on first launch; no manual `pip install` is required.",
8+
"author": {
9+
"name": "PyWry",
10+
"email": "pywry2@gmail.com",
11+
"url": "https://github.com/deeleeramone/PyWry"
12+
},
13+
"homepage": "https://github.com/deeleeramone/PyWry",
14+
"documentation": "https://github.com/deeleeramone/PyWry",
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/deeleeramone/PyWry.git"
18+
},
19+
"license": "Apache-2.0",
20+
"keywords": [
21+
"pywry",
22+
"webview",
23+
"tauri",
24+
"plotly",
25+
"tradingview",
26+
"aggrid",
27+
"dashboard",
28+
"charting",
29+
"visualization",
30+
"data-science",
31+
"finance",
32+
"interactive",
33+
"real-time",
34+
"chat",
35+
"llm",
36+
"agent",
37+
"artifact",
38+
"jupyter",
39+
"notebook",
40+
"python",
41+
"mcp"
42+
],
43+
"server": {
44+
"type": "uv",
45+
"entry_point": "src/server.py"
46+
},
47+
"tools_generated": true,
48+
"prompts_generated": true,
49+
"compatibility": {
50+
"claude_desktop": ">=0.10.0",
51+
"platforms": ["darwin", "win32", "linux"],
52+
"runtimes": {
53+
"python": ">=3.10"
54+
}
55+
}
56+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[project]
2+
name = "pywry-desktop-extension"
3+
version = "0.1.0"
4+
description = "PyWry MCP Bundle (.mcpb) for Claude Desktop. Wraps pywry[mcp] so the uv runtime resolves dependencies automatically."
5+
requires-python = ">=3.10"
6+
dependencies = [
7+
"pywry[mcp]>=2.0.0",
8+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Entry point for the PyWry MCP Bundle (.mcpb) loaded by Claude Desktop.
2+
3+
Claude Desktop's `uv` runtime resolves `pywry[mcp]` from the bundled
4+
`pyproject.toml`, then invokes this file. We re-enter the existing
5+
`pywry.mcp.__main__:main` so all 66+ tools, resources, and skill
6+
loaders stay in a single source tree.
7+
"""
8+
9+
import sys
10+
11+
from pywry.mcp.__main__ import main
12+
13+
14+
if __name__ == "__main__":
15+
sys.argv = ["pywry-mcp", "--transport", "stdio"]
16+
main()

0 commit comments

Comments
 (0)