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.
- protocolbox/__init__.py +3 -0
- protocolbox/cli.py +114 -0
- protocolbox/server.py +20 -0
- protocolbox/tools/__init__.py +7 -0
- protocolbox/tools/invoice.py +129 -0
- protocolbox/tools/json_healer.py +43 -0
- protocolbox/tools/scraper.py +61 -0
- protocolbox/tools/utils.py +26 -0
- protocolbox-0.1.0.dist-info/METADATA +112 -0
- protocolbox-0.1.0.dist-info/RECORD +13 -0
- protocolbox-0.1.0.dist-info/WHEEL +4 -0
- protocolbox-0.1.0.dist-info/entry_points.txt +2 -0
- protocolbox-0.1.0.dist-info/licenses/LICENSE +21 -0
protocolbox/__init__.py
ADDED
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)
|
|
29
|
+
[](https://python.org)
|
|
30
|
+
[](https://docs.astral.sh/ruff/)
|
|
31
|
+
[](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,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.
|