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