Initial commit

This commit is contained in:
caoimhinr 2026-06-02 23:07:29 +02:00
commit 6a1b671d79
3 changed files with 309 additions and 0 deletions

55
README.md Normal file
View file

@ -0,0 +1,55 @@
# pastebin-mcp
MCP server for the homelab pastebin service. Exposes create, read, list, and delete
operations as MCP tools so agents can interact with pastebin programmatically.
## Tools
| Tool | Auth required | Description |
|------|---------------|-------------|
| `create_paste` | No | Create a paste; returns URL and ID |
| `get_paste` | No | Fetch full paste metadata + content |
| `get_raw` | No | Fetch raw paste text only |
| `list_pastes` | Yes (API key) | List recent pastes with pagination |
| `delete_paste` | Yes (API key) | Permanently delete a paste |
## Environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `PASTEBIN_URL` | `http://192.168.0.242:3002` | Pastebin service base URL |
| `PASTEBIN_API_KEY` | *(empty)* | API key for privileged operations |
| `PASTEBIN_MCP_TRANSPORT` | `stdio` | Transport: `stdio`, `sse`, or `streamable-http` |
| `FASTMCP_HOST` | `127.0.0.1` | Bind host for HTTP transports |
| `FASTMCP_PORT` | `8000` | Bind port for HTTP transports |
| `FASTMCP_STREAMABLE_HTTP_PATH` | `/mcp` | HTTP path for streamable-http transport |
## Install
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```
## Run (HTTP / MCP endpoint)
```bash
PASTEBIN_MCP_TRANSPORT=streamable-http \
FASTMCP_HOST=0.0.0.0 \
FASTMCP_PORT=8004 \
PASTEBIN_API_KEY=<key> \
python mcp_server.py
```
## Run (stdio for Claude Desktop / claude mcp add)
```bash
PASTEBIN_API_KEY=<key> pastebin-mcp
```
## Register globally (HTTP)
```bash
claude mcp add --transport http --scope user pastebin-mcp http://192.168.0.242:8004/mcp
```

234
mcp_server.py Normal file
View file

@ -0,0 +1,234 @@
"""
pastebin-mcp MCP server for the homelab pastebin service.
Wraps the pastebin REST API so agents can create, retrieve, list, and
delete pastes programmatically.
Configure via environment variables:
PASTEBIN_URL Pastebin base URL (default http://192.168.0.242:3002)
PASTEBIN_API_KEY API key for privileged operations (list, delete)
Transport:
PASTEBIN_MCP_TRANSPORT stdio (default), sse, or streamable-http
FASTMCP_HOST bind host for HTTP transports (default 127.0.0.1)
FASTMCP_PORT bind port for HTTP transports (default 8000)
FASTMCP_STREAMABLE_HTTP_PATH path for streamable-http (default /mcp)
"""
import os
from typing import Optional
import httpx
from mcp.server.fastmcp import FastMCP
PASTEBIN_URL = os.environ.get("PASTEBIN_URL", "http://192.168.0.242:3002").rstrip("/")
PASTEBIN_API_KEY = os.environ.get("PASTEBIN_API_KEY", "")
MCP_TRANSPORT = os.getenv("PASTEBIN_MCP_TRANSPORT", "stdio").strip().lower() or "stdio"
MCP_HOST = os.getenv("FASTMCP_HOST", "127.0.0.1").strip() or "127.0.0.1"
MCP_PORT = int(os.getenv("FASTMCP_PORT", "8000"))
MCP_PATH = os.getenv("FASTMCP_STREAMABLE_HTTP_PATH", "/mcp").strip() or "/mcp"
mcp = FastMCP(
"pastebin-mcp",
host=MCP_HOST,
port=MCP_PORT,
streamable_http_path=MCP_PATH,
json_response=True,
stateless_http=True,
)
# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------
def _client() -> httpx.Client:
"""Return an httpx client pointed at the pastebin base URL."""
return httpx.Client(base_url=PASTEBIN_URL, timeout=10.0)
def _authed_client() -> httpx.Client:
"""Return an httpx client with the API key header set for privileged ops."""
return httpx.Client(
base_url=PASTEBIN_URL,
headers={"X-API-Key": PASTEBIN_API_KEY},
timeout=10.0,
)
def _check(r: httpx.Response) -> dict | list | str:
"""Raise on non-2xx responses; return parsed JSON or raw text."""
if not r.is_success:
raise RuntimeError(f"Pastebin API error {r.status_code}: {r.text}")
content_type = r.headers.get("content-type", "")
if "application/json" in content_type:
return r.json()
return r.text
# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------
@mcp.tool()
def create_paste(
content: str,
language: str = "plaintext",
title: str = "",
expires_in_hours: Optional[int] = None,
private: bool = False,
) -> dict:
"""
Create a new paste and return its URL and ID.
No authentication is required to create a paste. The paste is immediately
accessible at the returned URL.
Args:
content: The text content of the paste. May be any UTF-8 string.
language: Syntax-highlighting language hint, e.g. ``"python"``,
``"javascript"``, ``"yaml"``, ``"plaintext"`` (default).
The pastebin service uses this for display only.
title: Optional human-readable title for the paste. Empty string
means no title (default).
expires_in_hours: Optional number of hours until the paste expires and is
automatically deleted. ``None`` (default) means the paste
never expires.
private: If ``True``, the paste will not appear in public listings.
Defaults to ``False``.
Returns:
Dict with keys:
- ``id`` unique paste identifier (used in other tool calls)
- ``url`` full public URL to view the paste
- ``raw_url`` direct URL to the raw paste content
"""
payload: dict = {"content": content, "language": language, "private": private}
if title:
payload["title"] = title
if expires_in_hours is not None:
payload["expires_in_hours"] = expires_in_hours
with _client() as c:
result: dict = _check(c.post("/api/paste", json=payload)) # type: ignore[assignment]
paste_id = result.get("id", "")
return {
"id": paste_id,
"url": f"{PASTEBIN_URL}/paste/{paste_id}" if paste_id else "",
"raw_url": f"{PASTEBIN_URL}/raw/{paste_id}" if paste_id else "",
**result,
}
@mcp.tool()
def get_paste(paste_id: str) -> dict:
"""
Retrieve the full metadata and content for a single paste.
No authentication is required to read a paste.
Args:
paste_id: The unique identifier of the paste, as returned by
``create_paste`` or ``list_pastes``.
Returns:
Full paste object with keys including:
- ``id`` paste identifier
- ``content`` full text content
- ``language`` syntax language hint
- ``title`` paste title (may be empty)
- ``private`` whether the paste is private
- ``created_at`` ISO-8601 creation timestamp
- ``expires_at`` ISO-8601 expiry timestamp, or ``null`` if never
"""
with _client() as c:
return _check(c.get(f"/api/paste/{paste_id}")) # type: ignore[return-value]
@mcp.tool()
def get_raw(paste_id: str) -> str:
"""
Retrieve the raw text content of a paste as a plain string.
Useful when you only need the content itself without metadata overhead.
No authentication is required.
Args:
paste_id: The unique identifier of the paste, as returned by
``create_paste`` or ``list_pastes``.
Returns:
The raw text content of the paste as a plain string.
"""
with _client() as c:
return _check(c.get(f"/raw/{paste_id}")) # type: ignore[return-value]
@mcp.tool()
def list_pastes(limit: int = 50, offset: int = 0) -> list:
"""
List recent pastes from the pastebin service.
Requires the ``PASTEBIN_API_KEY`` environment variable to be set.
Returns pastes in reverse-chronological order (newest first).
Args:
limit: Maximum number of pastes to return (default 50). The server
may cap this at a lower value.
offset: Zero-based offset for pagination (default 0). Use in
combination with ``limit`` to page through results.
Returns:
List of paste summary objects, each containing:
- ``id`` paste identifier
- ``title`` paste title (may be empty)
- ``language`` syntax language hint
- ``private`` whether the paste is private
- ``created_at`` ISO-8601 creation timestamp
- ``expires_at`` ISO-8601 expiry timestamp, or ``null``
Note: the ``content`` field is typically omitted from listing responses
to reduce payload size. Use ``get_paste`` to fetch full content.
"""
with _authed_client() as c:
return _check(c.get("/api/pastes", params={"limit": limit, "offset": offset})) # type: ignore[return-value]
@mcp.tool()
def delete_paste(paste_id: str) -> dict:
"""
Permanently delete a paste.
Requires the ``PASTEBIN_API_KEY`` environment variable to be set.
This operation is irreversible.
Args:
paste_id: The unique identifier of the paste to delete, as returned
by ``create_paste`` or ``list_pastes``.
Returns:
Dict with:
- ``deleted`` ``True`` if the paste was successfully deleted
- ``paste_id`` the ID of the deleted paste
Raises ``RuntimeError`` if the paste does not exist or the API key
is missing or invalid.
"""
with _authed_client() as c:
r = c.delete(f"/api/paste/{paste_id}")
if not r.is_success:
raise RuntimeError(f"Pastebin API error {r.status_code}: {r.text}")
return {"deleted": True, "paste_id": paste_id}
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
if MCP_TRANSPORT not in {"stdio", "sse", "streamable-http"}:
raise ValueError("PASTEBIN_MCP_TRANSPORT must be stdio, sse, or streamable-http.")
mcp.run(transport=MCP_TRANSPORT)
if __name__ == "__main__":
main()

20
pyproject.toml Normal file
View file

@ -0,0 +1,20 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "pastebin-mcp"
version = "0.1.0"
description = "MCP server for the homelab pastebin service"
requires-python = ">=3.11"
dependencies = [
"mcp[cli]>=1.0",
"httpx>=0.28",
]
[project.scripts]
pastebin-mcp = "mcp_server:main"
[tool.setuptools.packages.find]
where = ["."]
include = ["pastebin_mcp*"]