pressbridge 1.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Negrea
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: pressbridge
3
+ Version: 1.1.0
4
+ Summary: Give Claude safe, audited access to your WordPress sites over MCP
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://pressbridgesupport.github.io/pressbridge
7
+ Project-URL: Source, https://github.com/pressbridgesupport/pressbridge
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: mcp<3,>=2.2
12
+ Requires-Dist: PyYAML>=6
13
+ Requires-Dist: uvicorn>=0.30
14
+ Requires-Dist: starlette>=0.40
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == "dev"
17
+ Requires-Dist: httpx>=0.27; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # PressBridge Free
21
+
22
+ <!-- mcp-name: io.github.pressbridgesupport/pressbridge -->
23
+
24
+ **Let Claude look inside your WordPress site safely: read-only, sandboxed, secrets hidden.**
25
+
26
+ [Website](https://pressbridgesupport.github.io/pressbridge) · [Demo video](https://youtu.be/hucKMlugERw) · [Get Pro](https://pressbridgesupport.github.io/pressbridge/#pricing) · [User guide](GUIDE.md) · [Security](SECURITY.md)
27
+
28
+ ```
29
+ you On mysite, where is the footer phone number set? And show me the DB password.
30
+
31
+ claude search "0744" in wp-content/themes → footer.php:41
32
+ read_file wp-config.php → refused: blocked by deny rule 'wp-config*'
33
+
34
+ The number is in wp-content/themes/mysite/footer.php, line 41.
35
+ I can't read wp-config.php: PressBridge hides it so passwords never reach me.
36
+ ```
37
+
38
+ PressBridge is an MCP server that gives Claude (Claude Code, Claude Desktop, any MCP
39
+ client) controlled access to a WordPress site. The Free edition is read-only and made
40
+ for one site: perfect for debugging, audits and "where is this text coming from?".
41
+
42
+ - Read, list and search theme and plugin files
43
+ - `wp-config.php`, `.env`, SQL dumps, keys and `.git` are invisible
44
+ - Nothing outside the site folder, symlink tricks included
45
+ - Read-only WP-CLI: versions, plugin and theme lists, checksums, cron, users
46
+ - `site_overview`: version and pending updates in one call
47
+ - Includes the **wp-care-report** skill: a client-ready health and security report
48
+
49
+ ## Install (5 minutes, nothing exposed to the internet)
50
+
51
+ On the server:
52
+
53
+ ```bash
54
+ sudo apt install python3-venv php-cli php-mysql
55
+ curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
56
+ sudo install wp-cli.phar /usr/local/bin/wp
57
+ python3 -m venv ~/pressbridge-venv
58
+ ~/pressbridge-venv/bin/pip install ./pressbridge
59
+ mkdir -p ~/.pressbridge
60
+ cat > ~/.pressbridge/config.yaml <<EOF
61
+ data_dir: $HOME/.pressbridge/data
62
+ sites:
63
+ mysite:
64
+ root: /var/www/mysite/public_html
65
+ EOF
66
+ ~/pressbridge-venv/bin/pressbridge check --config ~/.pressbridge/config.yaml
67
+ ```
68
+
69
+ Run it as the user that owns the site files (or one that can read them).
70
+
71
+ On your computer, Claude Code:
72
+
73
+ ```bash
74
+ claude mcp add pressbridge -- ssh you@server \
75
+ /home/you/pressbridge-venv/bin/pressbridge serve --config /home/you/.pressbridge/config.yaml
76
+ ```
77
+
78
+ Claude Desktop (`claude_desktop_config.json`, Settings → Developer → Edit Config):
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "pressbridge": {
84
+ "command": "ssh",
85
+ "args": ["-o", "BatchMode=yes", "you@server",
86
+ "/home/you/pressbridge-venv/bin/pressbridge", "serve",
87
+ "--config", "/home/you/.pressbridge/config.yaml"]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Then ask Claude: *"Give me a health overview of mysite"* or *"Where in the theme is the
94
+ footer phone number set?"*
95
+
96
+ The full user guide, with every command, config option and tool, is in
97
+ [GUIDE.md](GUIDE.md) (features marked **Pro** aren't in this edition).
98
+
99
+ ## Want Claude to fix things too?
100
+
101
+ **PressBridge Pro** adds editing with automatic backups and one-call undo, PHP syntax
102
+ checks before saving, plugin/theme/core updates, unlimited sites, a remote HTTPS server
103
+ with Docker and automatic certificates, the audit log, and skills for safe edits and
104
+ safe updates.
105
+
106
+ **→ https://pressbridgesupport.github.io/pressbridge**
107
+
108
+ ## License
109
+
110
+ MIT. See LICENSE.
@@ -0,0 +1,91 @@
1
+ # PressBridge Free
2
+
3
+ <!-- mcp-name: io.github.pressbridgesupport/pressbridge -->
4
+
5
+ **Let Claude look inside your WordPress site safely: read-only, sandboxed, secrets hidden.**
6
+
7
+ [Website](https://pressbridgesupport.github.io/pressbridge) · [Demo video](https://youtu.be/hucKMlugERw) · [Get Pro](https://pressbridgesupport.github.io/pressbridge/#pricing) · [User guide](GUIDE.md) · [Security](SECURITY.md)
8
+
9
+ ```
10
+ you On mysite, where is the footer phone number set? And show me the DB password.
11
+
12
+ claude search "0744" in wp-content/themes → footer.php:41
13
+ read_file wp-config.php → refused: blocked by deny rule 'wp-config*'
14
+
15
+ The number is in wp-content/themes/mysite/footer.php, line 41.
16
+ I can't read wp-config.php: PressBridge hides it so passwords never reach me.
17
+ ```
18
+
19
+ PressBridge is an MCP server that gives Claude (Claude Code, Claude Desktop, any MCP
20
+ client) controlled access to a WordPress site. The Free edition is read-only and made
21
+ for one site: perfect for debugging, audits and "where is this text coming from?".
22
+
23
+ - Read, list and search theme and plugin files
24
+ - `wp-config.php`, `.env`, SQL dumps, keys and `.git` are invisible
25
+ - Nothing outside the site folder, symlink tricks included
26
+ - Read-only WP-CLI: versions, plugin and theme lists, checksums, cron, users
27
+ - `site_overview`: version and pending updates in one call
28
+ - Includes the **wp-care-report** skill: a client-ready health and security report
29
+
30
+ ## Install (5 minutes, nothing exposed to the internet)
31
+
32
+ On the server:
33
+
34
+ ```bash
35
+ sudo apt install python3-venv php-cli php-mysql
36
+ curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
37
+ sudo install wp-cli.phar /usr/local/bin/wp
38
+ python3 -m venv ~/pressbridge-venv
39
+ ~/pressbridge-venv/bin/pip install ./pressbridge
40
+ mkdir -p ~/.pressbridge
41
+ cat > ~/.pressbridge/config.yaml <<EOF
42
+ data_dir: $HOME/.pressbridge/data
43
+ sites:
44
+ mysite:
45
+ root: /var/www/mysite/public_html
46
+ EOF
47
+ ~/pressbridge-venv/bin/pressbridge check --config ~/.pressbridge/config.yaml
48
+ ```
49
+
50
+ Run it as the user that owns the site files (or one that can read them).
51
+
52
+ On your computer, Claude Code:
53
+
54
+ ```bash
55
+ claude mcp add pressbridge -- ssh you@server \
56
+ /home/you/pressbridge-venv/bin/pressbridge serve --config /home/you/.pressbridge/config.yaml
57
+ ```
58
+
59
+ Claude Desktop (`claude_desktop_config.json`, Settings → Developer → Edit Config):
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "pressbridge": {
65
+ "command": "ssh",
66
+ "args": ["-o", "BatchMode=yes", "you@server",
67
+ "/home/you/pressbridge-venv/bin/pressbridge", "serve",
68
+ "--config", "/home/you/.pressbridge/config.yaml"]
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ Then ask Claude: *"Give me a health overview of mysite"* or *"Where in the theme is the
75
+ footer phone number set?"*
76
+
77
+ The full user guide, with every command, config option and tool, is in
78
+ [GUIDE.md](GUIDE.md) (features marked **Pro** aren't in this edition).
79
+
80
+ ## Want Claude to fix things too?
81
+
82
+ **PressBridge Pro** adds editing with automatic backups and one-call undo, PHP syntax
83
+ checks before saving, plugin/theme/core updates, unlimited sites, a remote HTTPS server
84
+ with Docker and automatic certificates, the audit log, and skills for safe edits and
85
+ safe updates.
86
+
87
+ **→ https://pressbridgesupport.github.io/pressbridge**
88
+
89
+ ## License
90
+
91
+ MIT. See LICENSE.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pressbridge"
7
+ version = "1.1.0"
8
+ license = "MIT"
9
+ description = "Give Claude safe, audited access to your WordPress sites over MCP"
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "mcp>=2.2,<3",
14
+ "PyYAML>=6",
15
+ "uvicorn>=0.30",
16
+ "starlette>=0.40",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://pressbridgesupport.github.io/pressbridge"
21
+ Source = "https://github.com/pressbridgesupport/pressbridge"
22
+
23
+ [project.optional-dependencies]
24
+ dev = ["pytest>=8", "httpx>=0.27"]
25
+
26
+ [project.scripts]
27
+ pressbridge = "pressbridge.__main__:main"
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """PressBridge: give Claude safe, audited access to WordPress sites."""
2
+
3
+ __version__ = "1.1.0"
@@ -0,0 +1,166 @@
1
+ """Command line entry point.
2
+
3
+ pressbridge serve --config config.yaml # stdio (Claude Desktop / Claude Code, local)
4
+ pressbridge serve --config config.yaml --http # HTTPS-ready HTTP server (Pro)
5
+ pressbridge check --config config.yaml # validate config and WP-CLI access
6
+ pressbridge token # print a new random token
7
+ pressbridge watch run|daemon|status|report|test-alert # security watch (Agent)
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import os
13
+ import secrets
14
+ import shutil
15
+ import sys
16
+
17
+ from . import __version__
18
+ from .config import ConfigError, load_config
19
+ from .edition import EDITION
20
+
21
+
22
+ def _cmd_check(cfg) -> int:
23
+ from . import wpcli
24
+
25
+ ok = True
26
+ print(f"PressBridge {__version__} ({EDITION})")
27
+ print(f"Data dir: {cfg.data_dir}")
28
+ if not shutil.which(cfg.wp_bin):
29
+ print(f" ! WP-CLI '{cfg.wp_bin}' not found on PATH")
30
+ ok = False
31
+ for s in cfg.sites.values():
32
+ print(f"- {s.name}: root={s.root} mode={s.mode} wp={s.wp_path or 'none'} profile={s.wp_profile}")
33
+ if s.wp_path and shutil.which(cfg.wp_bin):
34
+ r = wpcli.run(cfg, s, ["core", "version"])
35
+ if r["exit_code"] == 0:
36
+ print(f" WordPress {r['stdout'].strip()}")
37
+ else:
38
+ ok = False
39
+ print(f" ! WP-CLI failed: {r['stderr'].strip()[:300]}")
40
+ print("OK" if ok else "Problems found")
41
+ return 0 if ok else 1
42
+
43
+
44
+ def _cmd_watch(cfg, a) -> int:
45
+ if EDITION != "agent":
46
+ print("PressBridge Watch is part of PressBridge Agent (https://pressbridgesupport.github.io/pressbridge).", file=sys.stderr)
47
+ return 2
48
+ from .watch import parse_settings
49
+ from .watch import runner
50
+
51
+ try:
52
+ settings = parse_settings(cfg.watch)
53
+ except ConfigError as e:
54
+ print(f"Config error: {e}", file=sys.stderr)
55
+ return 2
56
+
57
+ if a.watch_cmd == "status":
58
+ print(runner.status(cfg, settings))
59
+ return 0
60
+ if a.watch_cmd == "report":
61
+ p = runner.watch_dir(cfg) / "reports" / "latest.txt"
62
+ print(p.read_text() if p.exists() else "No report yet. Run: pressbridge watch run")
63
+ return 0
64
+ if a.watch_cmd == "test-alert":
65
+ errors = runner.test_alert(settings)
66
+ for e in errors:
67
+ print(f"! {e}", file=sys.stderr)
68
+ if not errors:
69
+ print("Test alert sent.")
70
+ return 1 if errors else 0
71
+ if a.watch_cmd == "daemon":
72
+ try:
73
+ runner.daemon(cfg, settings, run_now=a.now, log=lambda m: print(m, flush=True))
74
+ except KeyboardInterrupt:
75
+ pass
76
+ return 0
77
+
78
+ try:
79
+ result = runner.run_once(cfg, settings, sites=a.site, notify=not a.no_notify,
80
+ use_claude=not a.no_claude, force_notify=a.notify_always,
81
+ save=not a.dry_run)
82
+ except (RuntimeError, ValueError) as e:
83
+ print(f"Error: {e}", file=sys.stderr)
84
+ return 3
85
+ print(result.report_path.read_text() if result.report_path else "")
86
+ if result.notified:
87
+ print("Alert sent.")
88
+ for e in result.delivery_errors:
89
+ print(f"! Alert delivery failed: {e}", file=sys.stderr)
90
+ return result.exit_code
91
+
92
+
93
+ def main(argv: list[str] | None = None) -> int:
94
+ p = argparse.ArgumentParser(prog="pressbridge", description="Safe WordPress access for Claude")
95
+ p.add_argument("--version", action="version", version=f"pressbridge {__version__} ({EDITION})")
96
+ sub = p.add_subparsers(dest="cmd", required=True)
97
+
98
+ serve = sub.add_parser("serve", help="Run the MCP server")
99
+ serve.add_argument("--config", default=os.environ.get("PRESSBRIDGE_CONFIG", "/config/config.yaml"))
100
+ serve.add_argument("--http", action="store_true", help="Serve over HTTP instead of stdio (Pro)")
101
+ serve.add_argument("--port", type=int, default=int(os.environ.get("PRESSBRIDGE_PORT", "8765")))
102
+ serve.add_argument("--host", default=os.environ.get("PRESSBRIDGE_HOST", "0.0.0.0"),
103
+ help="Interface to bind in HTTP mode (use 127.0.0.1 behind a proxy on the host network)")
104
+
105
+ check = sub.add_parser("check", help="Validate the config and WP-CLI access")
106
+ check.add_argument("--config", default=os.environ.get("PRESSBRIDGE_CONFIG", "/config/config.yaml"))
107
+
108
+ sub.add_parser("token", help="Generate a random bearer token")
109
+
110
+ watch = sub.add_parser("watch", help="Security watch: nightly checks and alerts (Agent edition)")
111
+ watch.add_argument("--config", default=os.environ.get("PRESSBRIDGE_CONFIG", "/config/config.yaml"))
112
+ wsub = watch.add_subparsers(dest="watch_cmd", required=True)
113
+ w_run = wsub.add_parser("run", help="Check all sites once now")
114
+ w_run.add_argument("--site", action="append", help="Only this site (repeatable)")
115
+ w_run.add_argument("--no-notify", action="store_true", help="Don't send alerts")
116
+ w_run.add_argument("--no-claude", action="store_true", help="Skip Claude's review")
117
+ w_run.add_argument("--notify-always", action="store_true", help="Send the report even if all is clear")
118
+ w_run.add_argument("--dry-run", action="store_true", help="Don't save the new baseline (compare only)")
119
+ w_daemon = wsub.add_parser("daemon", help="Run on the schedule, forever (for Docker or systemd)")
120
+ w_daemon.add_argument("--now", action="store_true", help="Also run once at start")
121
+ wsub.add_parser("status", help="Show the last run, the next run and each site's baseline")
122
+ wsub.add_parser("test-alert", help="Send a test alert to every configured channel")
123
+ wsub.add_parser("report", help="Print the latest report")
124
+
125
+ a = p.parse_args(argv)
126
+ if a.cmd == "token":
127
+ print(secrets.token_hex(32))
128
+ return 0
129
+
130
+ try:
131
+ cfg = load_config(a.config)
132
+ except ConfigError as e:
133
+ print(f"Config error: {e}", file=sys.stderr)
134
+ return 2
135
+
136
+ if a.cmd == "check":
137
+ return _cmd_check(cfg)
138
+
139
+ if a.cmd == "watch":
140
+ return _cmd_watch(cfg, a)
141
+
142
+ if a.http:
143
+ if EDITION == "free":
144
+ print("HTTP mode is a PressBridge Pro feature. Free runs over stdio.", file=sys.stderr)
145
+ return 2
146
+ import uvicorn
147
+
148
+ from .server import build_http_app
149
+
150
+ try:
151
+ app = build_http_app(cfg)
152
+ except ConfigError as e:
153
+ print(f"Config error: {e}", file=sys.stderr)
154
+ return 2
155
+ uvicorn.run(app, host=a.host, port=a.port, log_level="info", proxy_headers=True,
156
+ forwarded_allow_ips="*")
157
+ return 0
158
+
159
+ from .server import build_server
160
+
161
+ build_server(cfg).run("stdio")
162
+ return 0
163
+
164
+
165
+ if __name__ == "__main__":
166
+ sys.exit(main())
@@ -0,0 +1,58 @@
1
+ """Append-only JSON Lines audit log of every tool call."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import threading
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ _lock = threading.Lock()
10
+ _MAX_FIELD = 500
11
+
12
+
13
+ def _shorten(value):
14
+ if isinstance(value, str) and len(value) > _MAX_FIELD:
15
+ return value[:_MAX_FIELD] + f"... [{len(value)} chars]"
16
+ if isinstance(value, list):
17
+ return [_shorten(v) for v in value]
18
+ if isinstance(value, dict):
19
+ return {k: _shorten(v) for k, v in value.items()}
20
+ return value
21
+
22
+
23
+ def record(log_path: Path, *, tool: str, site: str | None, args: dict, ok: bool, error: str | None = None,
24
+ result_summary: dict | None = None) -> None:
25
+ entry = {
26
+ "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"),
27
+ "tool": tool,
28
+ "site": site,
29
+ "args": _shorten(args),
30
+ "ok": ok,
31
+ }
32
+ if error:
33
+ entry["error"] = error[:_MAX_FIELD]
34
+ if result_summary:
35
+ entry["result"] = result_summary
36
+ line = json.dumps(entry, ensure_ascii=False)
37
+ with _lock:
38
+ log_path.parent.mkdir(parents=True, exist_ok=True)
39
+ with log_path.open("a", encoding="utf-8") as f:
40
+ f.write(line + "\n")
41
+
42
+
43
+ def tail(log_path: Path, limit: int = 50, site: str | None = None) -> list[dict]:
44
+ if not log_path.exists():
45
+ return []
46
+ lines = log_path.read_text(encoding="utf-8").splitlines()
47
+ out: list[dict] = []
48
+ for line in reversed(lines):
49
+ try:
50
+ item = json.loads(line)
51
+ except json.JSONDecodeError:
52
+ continue
53
+ if site and item.get("site") != site:
54
+ continue
55
+ out.append(item)
56
+ if len(out) >= limit:
57
+ break
58
+ return out