protocolbox 0.1.0__py3-none-any.whl

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,3 @@
1
+ """ProtocolBox — The Standard Library for the Agentic Web."""
2
+
3
+ __version__ = "0.1.0"
protocolbox/cli.py ADDED
@@ -0,0 +1,114 @@
1
+ """ProtocolBox CLI — auto-setup for AI Agent environments."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ app = typer.Typer(
9
+ name="protocolbox",
10
+ help="ProtocolBox — The Standard Library for the Agentic Web.",
11
+ no_args_is_help=True,
12
+ )
13
+
14
+ SKILL_TEMPLATE = """---
15
+ name: protocolbox
16
+ description: >
17
+ Standard Library of verified tools for AI Agents.
18
+ Tools: scrape(url), heal_json(str), generate_invoice(dict).
19
+ ---
20
+
21
+ # ProtocolBox
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install protocolbox
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ Start the MCP server:
32
+
33
+ ```bash
34
+ protocolbox start
35
+ ```
36
+
37
+ Or with uv:
38
+
39
+ ```bash
40
+ uv run protocolbox start
41
+ ```
42
+
43
+ ## Available Tools
44
+
45
+ - **scrape(url: str) -> str** — Fetch a web page and return clean Markdown.
46
+ - **heal_json(broken_json: str) -> dict** — Fix malformed JSON from LLM output.
47
+ - **generate_invoice(data: dict) -> str** — Generate a PDF invoice.
48
+ Requires `client_name` and `total` in data.
49
+ """
50
+
51
+
52
+ def _detect_antigravity() -> Path | None:
53
+ """Detect if running in an Antigravity environment.
54
+
55
+ Returns the config directory path if detected, None otherwise.
56
+ """
57
+ # Check environment variable first
58
+ antigravity_home = os.environ.get("ANTIGRAVITY_HOME")
59
+ if antigravity_home:
60
+ return Path(antigravity_home)
61
+
62
+ # Check for ~/.antigravity directory
63
+ antigravity_dir = Path.home() / ".antigravity"
64
+ if antigravity_dir.exists():
65
+ return antigravity_dir
66
+
67
+ return None
68
+
69
+
70
+ def _write_skill_file(target_dir: Path) -> Path:
71
+ """Write the SKILL.md file to the target directory.
72
+
73
+ Returns the path to the created file.
74
+ """
75
+ target_dir.mkdir(parents=True, exist_ok=True)
76
+ skill_path = target_dir / "SKILL.md"
77
+ skill_path.write_text(SKILL_TEMPLATE.strip() + "\n")
78
+ return skill_path
79
+
80
+
81
+ @app.command()
82
+ def init() -> None:
83
+ """Initialize ProtocolBox for your AI Agent environment."""
84
+ typer.echo("🔧 ProtocolBox Init")
85
+ typer.echo("=" * 40)
86
+
87
+ antigravity_dir = _detect_antigravity()
88
+
89
+ if antigravity_dir:
90
+ typer.echo(f"✅ Antigravity environment detected: {antigravity_dir}")
91
+ skill_path = _write_skill_file(antigravity_dir / "skills" / "protocolbox")
92
+ typer.echo(f"📄 SKILL.md written to: {skill_path}")
93
+ else:
94
+ typer.echo("ℹ️ No Antigravity environment detected.")
95
+ # Write to current directory as fallback
96
+ skill_path = _write_skill_file(Path.cwd() / ".protocolbox")
97
+ typer.echo(f"📄 SKILL.md written to: {skill_path}")
98
+
99
+ typer.echo()
100
+ typer.echo("🚀 Ready! Start the server with:")
101
+ typer.echo(" protocolbox start")
102
+
103
+
104
+ @app.command()
105
+ def start() -> None:
106
+ """Start the ProtocolBox MCP server."""
107
+ typer.echo("🚀 Starting ProtocolBox MCP Server...")
108
+ from protocolbox.server import main
109
+
110
+ main()
111
+
112
+
113
+ if __name__ == "__main__":
114
+ app()
protocolbox/server.py ADDED
@@ -0,0 +1,20 @@
1
+ """ProtocolBox MCP Server — the engine that exposes all tools."""
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ mcp = FastMCP("ProtocolBox")
6
+
7
+ # Import tools so they register with the MCP server.
8
+ # Each tool module uses the `mcp` instance via import.
9
+ import protocolbox.tools.invoice # noqa: F401, E402
10
+ import protocolbox.tools.json_healer # noqa: F401, E402
11
+ import protocolbox.tools.scraper # noqa: F401, E402
12
+
13
+
14
+ def main() -> None:
15
+ """Run the ProtocolBox MCP server."""
16
+ mcp.run()
17
+
18
+
19
+ if __name__ == "__main__":
20
+ main()
@@ -0,0 +1,7 @@
1
+ """ProtocolBox Tools — the standard library of MCP tools."""
2
+
3
+ from protocolbox.tools.invoice import generate_invoice
4
+ from protocolbox.tools.json_healer import heal_json
5
+ from protocolbox.tools.scraper import scrape
6
+
7
+ __all__ = ["scrape", "heal_json", "generate_invoice"]
@@ -0,0 +1,129 @@
1
+ """pb_invoice — Generate PDF invoices from structured data."""
2
+
3
+ import uuid
4
+ from typing import Any
5
+
6
+ from reportlab.lib.pagesizes import A4
7
+ from reportlab.lib.units import mm
8
+ from reportlab.pdfgen import canvas
9
+
10
+ from protocolbox.server import mcp
11
+
12
+
13
+ @mcp.tool()
14
+ def generate_invoice(data: dict[str, Any]) -> str:
15
+ """Generate a PDF invoice from structured data.
16
+
17
+ The data dict must contain at minimum:
18
+ - client_name (str): Name of the client.
19
+ - total (float | int): Invoice total amount.
20
+
21
+ Optional fields:
22
+ - invoice_number (str): Custom invoice number.
23
+ - items (list[dict]): Line items with 'description', 'qty', 'price'.
24
+ - currency (str): Currency symbol (default: "$").
25
+ - notes (str): Additional notes for the invoice.
26
+
27
+ Args:
28
+ data: Dictionary containing invoice details.
29
+
30
+ Returns:
31
+ The file path to the generated PDF, or an error message.
32
+ """
33
+ # --- Validate required fields ---
34
+ client_name = data.get("client_name")
35
+ total = data.get("total")
36
+
37
+ if not client_name:
38
+ return "Error: 'client_name' is required in the data dict."
39
+ if total is None:
40
+ return "Error: 'total' is required in the data dict."
41
+
42
+ try:
43
+ total = float(total)
44
+ except (TypeError, ValueError):
45
+ return "Error: 'total' must be a number."
46
+
47
+ # --- Set up defaults ---
48
+ invoice_number = data.get("invoice_number", f"INV-{uuid.uuid4().hex[:8].upper()}")
49
+ currency = data.get("currency", "$")
50
+ items: list[dict[str, Any]] = data.get("items", [])
51
+ notes: str = data.get("notes", "")
52
+
53
+ # --- Generate PDF ---
54
+ file_path = f"/tmp/invoice_{uuid.uuid4().hex}.pdf"
55
+ c = canvas.Canvas(file_path, pagesize=A4)
56
+ width, height = A4
57
+
58
+ # Header
59
+ c.setFont("Helvetica-Bold", 24)
60
+ c.drawString(30 * mm, height - 30 * mm, "INVOICE")
61
+
62
+ c.setFont("Helvetica", 10)
63
+ c.drawString(30 * mm, height - 40 * mm, f"Invoice #: {invoice_number}")
64
+
65
+ # Client info
66
+ c.setFont("Helvetica-Bold", 12)
67
+ c.drawString(30 * mm, height - 55 * mm, "Bill To:")
68
+ c.setFont("Helvetica", 11)
69
+ c.drawString(30 * mm, height - 62 * mm, str(client_name))
70
+
71
+ # Line items
72
+ y_pos = height - 80 * mm
73
+
74
+ if items:
75
+ # Table header
76
+ c.setFont("Helvetica-Bold", 10)
77
+ c.drawString(30 * mm, y_pos, "Description")
78
+ c.drawString(120 * mm, y_pos, "Qty")
79
+ c.drawString(145 * mm, y_pos, "Price")
80
+ c.drawString(170 * mm, y_pos, "Subtotal")
81
+ y_pos -= 7 * mm
82
+
83
+ # Draw a line
84
+ c.line(30 * mm, y_pos + 3 * mm, 190 * mm, y_pos + 3 * mm)
85
+
86
+ c.setFont("Helvetica", 10)
87
+ for item in items:
88
+ desc = str(item.get("description", "—"))
89
+ qty = item.get("qty", 1)
90
+ price = item.get("price", 0)
91
+ subtotal = float(qty) * float(price)
92
+
93
+ c.drawString(30 * mm, y_pos, desc[:40])
94
+ c.drawString(120 * mm, y_pos, str(qty))
95
+ c.drawString(145 * mm, y_pos, f"{currency}{price:.2f}")
96
+ c.drawString(170 * mm, y_pos, f"{currency}{subtotal:.2f}")
97
+ y_pos -= 6 * mm
98
+
99
+ y_pos -= 5 * mm
100
+
101
+ # Total
102
+ c.line(30 * mm, y_pos + 3 * mm, 190 * mm, y_pos + 3 * mm)
103
+ c.setFont("Helvetica-Bold", 14)
104
+ c.drawString(145 * mm, y_pos - 5 * mm, f"Total: {currency}{total:.2f}")
105
+ y_pos -= 20 * mm
106
+
107
+ # Notes
108
+ if notes:
109
+ c.setFont("Helvetica-Oblique", 9)
110
+ c.drawString(30 * mm, y_pos, f"Notes: {notes}")
111
+
112
+ # Footer
113
+ c.setFont("Helvetica", 8)
114
+ c.drawString(30 * mm, 15 * mm, "Generated by ProtocolBox — protocolbox.in")
115
+
116
+ c.save()
117
+
118
+ return file_path
119
+
120
+
121
+ def _validate_invoice_data(data: dict[str, Any]) -> str | None:
122
+ """Validate invoice data, returning an error message or None."""
123
+ if not isinstance(data, dict):
124
+ return "Error: Input must be a dictionary."
125
+ if "client_name" not in data:
126
+ return "Error: 'client_name' is required."
127
+ if "total" not in data:
128
+ return "Error: 'total' is required."
129
+ return None
@@ -0,0 +1,43 @@
1
+ """pb_heal_json — Fix malformed JSON from LLM output."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import json_repair
7
+
8
+ from protocolbox.server import mcp
9
+
10
+
11
+ @mcp.tool()
12
+ def heal_json(broken_json: str) -> dict[str, Any]:
13
+ """Attempt to repair malformed JSON and return a valid Python dict.
14
+
15
+ Handles common LLM output issues: trailing commas, unquoted keys,
16
+ single quotes, truncated output, and more.
17
+
18
+ Args:
19
+ broken_json: A string containing malformed or broken JSON.
20
+
21
+ Returns:
22
+ A valid Python dictionary parsed from the repaired JSON,
23
+ or an error dict if repair is impossible.
24
+ """
25
+ try:
26
+ repaired = json_repair.repair_json(broken_json, return_objects=False)
27
+
28
+ if not isinstance(repaired, str):
29
+ repaired = str(repaired)
30
+
31
+ result = json.loads(repaired)
32
+
33
+ # Ensure we always return a dict.
34
+ if isinstance(result, dict):
35
+ return result
36
+ elif isinstance(result, list):
37
+ return {"data": result}
38
+ else:
39
+ return {"value": result}
40
+
41
+ except (json.JSONDecodeError, TypeError, ValueError):
42
+ snippet = broken_json[:100] + ("..." if len(broken_json) > 100 else "")
43
+ return {"error": "Failed", "input_snippet": snippet}
@@ -0,0 +1,61 @@
1
+ """pb_scrape — Token-efficient web page scraper."""
2
+
3
+ import re
4
+
5
+ import html2text
6
+ import httpx
7
+
8
+ from protocolbox.server import mcp
9
+ from protocolbox.tools.utils import DEFAULT_TIMEOUT, DEFAULT_USER_AGENT
10
+
11
+
12
+ @mcp.tool()
13
+ def scrape(url: str) -> str:
14
+ """Fetch a web page and return its content as clean Markdown.
15
+
16
+ Strips scripts, styles, and footers for token-efficient reading.
17
+
18
+ Args:
19
+ url: The URL of the web page to scrape.
20
+
21
+ Returns:
22
+ The page content as Markdown, or an error message.
23
+ """
24
+ try:
25
+ response = httpx.get(
26
+ url,
27
+ timeout=DEFAULT_TIMEOUT,
28
+ headers={"User-Agent": DEFAULT_USER_AGENT},
29
+ follow_redirects=True,
30
+ )
31
+ response.raise_for_status()
32
+ except httpx.HTTPStatusError as e:
33
+ return f"Error: Unable to access page. HTTP {e.response.status_code}."
34
+ except httpx.RequestError as e:
35
+ return f"Error: Unable to access page. {type(e).__name__}: {e}"
36
+
37
+ html = response.text
38
+
39
+ # Strip <script>, <style>, and <footer> tags and their contents.
40
+ html = re.sub(
41
+ r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE
42
+ )
43
+ html = re.sub(
44
+ r"<style[^>]*>.*?</style>", "", html, flags=re.DOTALL | re.IGNORECASE
45
+ )
46
+ html = re.sub(
47
+ r"<footer[^>]*>.*?</footer>", "", html, flags=re.DOTALL | re.IGNORECASE
48
+ )
49
+
50
+ # Convert to Markdown.
51
+ converter = html2text.HTML2Text()
52
+ converter.ignore_links = False
53
+ converter.ignore_images = True
54
+ converter.body_width = 0 # No line wrapping
55
+
56
+ markdown = converter.handle(html)
57
+
58
+ # Clean up excessive blank lines.
59
+ markdown = re.sub(r"\n{3,}", "\n\n", markdown).strip()
60
+
61
+ return markdown
@@ -0,0 +1,26 @@
1
+ """Shared utilities for ProtocolBox tools."""
2
+
3
+ import httpx
4
+
5
+ # Standard timeout for HTTP requests (seconds).
6
+ DEFAULT_TIMEOUT = 10.0
7
+
8
+ # Generic User-Agent to avoid bot detection.
9
+ DEFAULT_USER_AGENT = (
10
+ "Mozilla/5.0 (compatible; ProtocolBox/0.1; +https://protocolbox.in)"
11
+ )
12
+
13
+
14
+ def get_http_client(**kwargs: object) -> httpx.Client:
15
+ """Create a pre-configured httpx Client.
16
+
17
+ Uses standard timeout and User-Agent defaults.
18
+ Additional kwargs are passed through to httpx.Client.
19
+ """
20
+ headers = {"User-Agent": DEFAULT_USER_AGENT}
21
+ return httpx.Client(
22
+ timeout=DEFAULT_TIMEOUT,
23
+ headers=headers,
24
+ follow_redirects=True,
25
+ **kwargs, # type: ignore[arg-type]
26
+ )
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: protocolbox
3
+ Version: 0.1.0
4
+ Summary: The Standard Library for the Agentic Web — verified MCP tools for any AI Agent.
5
+ Project-URL: Homepage, https://protocolbox.in
6
+ Project-URL: Repository, https://github.com/ianuragbhatt/protocolbox
7
+ Project-URL: Issues, https://github.com/ianuragbhatt/protocolbox/issues
8
+ Author-email: Anurag Bhatt <anurag@protocolbox.in>
9
+ License-File: LICENSE
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: html2text
16
+ Requires-Dist: httpx
17
+ Requires-Dist: json-repair
18
+ Requires-Dist: mcp>=0.1.0
19
+ Requires-Dist: reportlab
20
+ Requires-Dist: typer
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest; extra == 'dev'
23
+ Requires-Dist: ruff; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # ProtocolBox 📦
27
+
28
+ [![License: MIT](https://img.shields.io/badge/License-MIT-white.svg)](LICENSE)
29
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-white.svg)](https://python.org)
30
+ [![Ruff](https://img.shields.io/badge/linting-ruff-white.svg)](https://docs.astral.sh/ruff/)
31
+ [![MCP](https://img.shields.io/badge/protocol-MCP-white.svg)](https://modelcontextprotocol.io/)
32
+
33
+ > **The Standard Library for the Agentic Web.**
34
+ > https://protocolbox.in
35
+
36
+ ProtocolBox is a collection of high-reliability **[MCP (Model Context Protocol)](https://modelcontextprotocol.io/)** tools designed for AI Agents. It provides verified, token-efficient utilities that work out-of-the-box with Claude, Gemini, and other MCP-compliant agents.
37
+
38
+ ## 🚀 Installation
39
+
40
+ ```bash
41
+ pip install protocolbox
42
+ ```
43
+
44
+ Initialize the configuration for your agent:
45
+
46
+ ```bash
47
+ protocolbox init
48
+ ```
49
+
50
+ ## 🛠️ Tools
51
+
52
+ ProtocolBox currently exports 3 core tools optimized for agent workflows:
53
+
54
+ | Tool | Signature | Description |
55
+ | :--- | :--- | :--- |
56
+ | **Scrape** | `scrape(url: str) -> str` | Fetches a webpage and converts it to clean, token-saving Markdown. Removes ads, scripts, and clutter automatically. |
57
+ | **Heal JSON** | `heal_json(json_str: str) -> dict` | repairs malformed JSON strings often produced by LLMs (trailing commas, missing quotes, etc.) into valid Python dictionaries. |
58
+ | **Invoice** | `generate_invoice(data: dict) -> str` | Generates a professional PDF invoice from structured data in milliseconds. |
59
+
60
+ ## ⚡ Usage
61
+
62
+ Start the MCP server to expose these tools to your agent:
63
+
64
+ ```bash
65
+ protocolbox start
66
+ ```
67
+
68
+ Or using `uv`:
69
+
70
+ ```bash
71
+ uv run protocolbox start
72
+ ```
73
+
74
+ ## 📦 Project Structure
75
+
76
+ ```text
77
+ protocolbox/
78
+ ├── src/protocolbox/ # Core package
79
+ │ ├── server.py # FastMCP server
80
+ │ ├── cli.py # CLI entry point
81
+ │ └── tools/ # Tool implementations
82
+ ├── tests/ # 115+ edge-case tests
83
+ ├── docs/ # Documentation site
84
+ └── pyproject.toml # Project config
85
+ ```
86
+
87
+ ## 👨‍💻 Development
88
+
89
+ We recommend [uv](https://docs.astral.sh/uv/) for a fast, reliable dev environment.
90
+
91
+ ```bash
92
+ # Clone and setup
93
+ git clone https://github.com/ianuragbhatt/protocolbox.git
94
+ cd protocolbox
95
+ uv pip install -e ".[dev]"
96
+
97
+ # Run tests (100% pass rate required)
98
+ pytest tests/ -v
99
+
100
+ # Linting
101
+ ruff check .
102
+ ```
103
+
104
+ ## 🤝 Contributing
105
+
106
+ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to add new tools.
107
+
108
+ **Maintainer:** [Anurag Bhatt (@ianuragbhatt)](https://github.com/ianuragbhatt)
109
+
110
+ ## License
111
+
112
+ MIT © 2026 ProtocolBox.
@@ -0,0 +1,13 @@
1
+ protocolbox/__init__.py,sha256=yRIuWez8clGaDQvLIIvPRgp6R2WyxYN4-SDq5AyEVWo,87
2
+ protocolbox/cli.py,sha256=5CIf2GRUr5rLBGdAIEPDoz5YUwUlNWqabjYhM1d4i5U,2812
3
+ protocolbox/server.py,sha256=hBr3WbfwPLB8j6LJcpzvqfYC6ZEZJ3fGajgokoG3M-Y,528
4
+ protocolbox/tools/__init__.py,sha256=q3NL-ftBR8K-hWLOMORpIECUTT6hbRyHK4iz6z1waU4,271
5
+ protocolbox/tools/invoice.py,sha256=Pc0OSLcG7omo5FBbQ4V9KyNGSrxMj6E7qtzFtS_XTTU,4032
6
+ protocolbox/tools/json_healer.py,sha256=l5ZRcBIGrGyZ6dh_QX0cMqlADBo3jszg7chViHh5Mls,1259
7
+ protocolbox/tools/scraper.py,sha256=zGqSkSTWls7R1-yJPSJFauNwbe7adaKPZRtVR3jLuRY,1731
8
+ protocolbox/tools/utils.py,sha256=nRIojE-7A9rzoSzkNfvHS63Ne44g1J7dmFDT4FQFOUg,709
9
+ protocolbox-0.1.0.dist-info/METADATA,sha256=WQ75WE9Hgh4dPev-uZlozEn-ROSmPTaZnYhtfhtTIDo,3572
10
+ protocolbox-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ protocolbox-0.1.0.dist-info/entry_points.txt,sha256=E8JWSJCLiE9mdZVXT-ufHxrEAXLliuLRY7RhuFx5_WQ,52
12
+ protocolbox-0.1.0.dist-info/licenses/LICENSE,sha256=kn261Rb1pbwzB0zRWTFmInRd95RKhgDaxjKRuhVqf4Y,1069
13
+ protocolbox-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ protocolbox = protocolbox.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anurag Bhatt
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.