Initial commit
This commit is contained in:
commit
6a1b671d79
3 changed files with 309 additions and 0 deletions
55
README.md
Normal file
55
README.md
Normal 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
234
mcp_server.py
Normal 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
20
pyproject.toml
Normal 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*"]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue