Initial skills documentation — 25 categories, all SKILL.md + references + scripts
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# FastMCP + Hermes MCP Server Deployment Pattern
|
||||
|
||||
> Super Search (Jul 2026) reference implementation
|
||||
|
||||
## Architecture
|
||||
|
||||
Hermes supports **two MCP server transport modes** for custom tools:
|
||||
|
||||
| Mode | Config | Use Case |
|
||||
|------|--------|----------|
|
||||
| **Command** | `command: python3` + `args: [...]` | Subprocess managed by Hermes |
|
||||
| **HTTP/URL** | `url: http://127.0.0.1:PORT/mcp` | Standalone service (systemd/Docker) |
|
||||
|
||||
For production services with auto-restart, use **HTTP mode** behind a systemd service.
|
||||
|
||||
## Build Pattern
|
||||
|
||||
### 1. FastMCP Server
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Service Name", version="1.0.0")
|
||||
|
||||
@mcp.tool(description="Tool description shown to Hermes.")
|
||||
async def my_tool(query: str, limit: int = 5) -> str:
|
||||
"""Tool docstring."""
|
||||
# implementation
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http", host="127.0.0.1", port=8899, path="/mcp")
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `transport="http"` exposes SSE + JSON-RPC on the HTTP path
|
||||
- Bind to `127.0.0.1` (loopback) — never expose MCP endpoints publicly
|
||||
- Each `@mcp.tool` becomes a discoverable tool in Hermes
|
||||
|
||||
### 2. Virtual Environment
|
||||
|
||||
```bash
|
||||
python3 -m venv /path/to/service/venv
|
||||
/path/to/service/venv/bin/pip install fastmcp httpx trafilatura # etc.
|
||||
```
|
||||
|
||||
### 3. Environment Variables
|
||||
|
||||
Secrets via `.env` file with systemd `EnvironmentFile` directive:
|
||||
|
||||
```
|
||||
MY_API_KEY=sk-...
|
||||
```
|
||||
|
||||
Load in Python with `python-dotenv`:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path.home() / ".hermes" / ".env")
|
||||
# or use a local .env:
|
||||
# load_dotenv(Path(__file__).parent / ".env")
|
||||
```
|
||||
|
||||
### 4. Systemd Service
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=My MCP Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/path/to/service
|
||||
Environment="PATH=/path/to/service/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
Environment="PYTHONUNBUFFERED=1"
|
||||
EnvironmentFile=/path/to/.env
|
||||
ExecStart=/path/to/service/venv/bin/python3 /path/to/service/server.py
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Install:
|
||||
```bash
|
||||
cp service.service /etc/systemd/system/
|
||||
systemctl daemon-reload && systemctl enable --now my-service
|
||||
```
|
||||
|
||||
### 5. Hermes MCP Config Wiring
|
||||
|
||||
Add to `~/.hermes/config.yaml` under `mcp_servers:`:
|
||||
|
||||
```yaml
|
||||
mcp_servers:
|
||||
my-service:
|
||||
url: http://127.0.0.1:8899/mcp
|
||||
enabled: true
|
||||
```
|
||||
|
||||
Or via CLI:
|
||||
```bash
|
||||
hermes config set mcp_servers.my-service.url 'http://127.0.0.1:8899/mcp'
|
||||
hermes config set mcp_servers.my-service.enabled 'true'
|
||||
```
|
||||
|
||||
### 6. Verification
|
||||
|
||||
```bash
|
||||
systemctl is-active my-service
|
||||
hermes mcp test my-service
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Testing 'my-service'...
|
||||
Transport: HTTP → http://127.0.0.1:8899/mcp
|
||||
Auth: none
|
||||
✓ Connected (XXms)
|
||||
✓ Tools discovered: N
|
||||
```
|
||||
|
||||
### 7. Gateway Restart (if tools don't appear in session)
|
||||
|
||||
```bash
|
||||
# From outside the gateway session (SSH):
|
||||
systemctl restart hermes.service
|
||||
|
||||
# Avoid using 'hermes gateway restart' from inside the Telegram session
|
||||
# — it kills the process you're talking through.
|
||||
```
|
||||
|
||||
## Multi-Provider Fallback Pattern
|
||||
|
||||
For services that combine multiple backends (search, AI, extraction), use an ordered fallback chain:
|
||||
|
||||
```python
|
||||
async def web_search(query, limit=5):
|
||||
# Tier 1 — Free/self-hosted
|
||||
try:
|
||||
results = await free_backend(query, limit)
|
||||
if results:
|
||||
return results
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Tier 2 — Premium API
|
||||
try:
|
||||
results = await premium_backend(query, limit)
|
||||
if results:
|
||||
return results
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Tier 3 — Paid fallback (last resort)
|
||||
try:
|
||||
return await paid_backend(query, limit)
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
```
|
||||
|
||||
Each tier catches exceptions independently so one backend's failure doesn't cascade.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **Systemd `EnvironmentFile` loads from `.env` format** — lines must be `KEY=VALUE`. No export, no quotes around values (unlike shell). The file is read by systemd, not sourced by bash.
|
||||
- **FastMCP HTTP transport requires `Accept: application/json, text/event-stream` header** — simple `curl` requests without this header get a 406 error. Hermes' MCP client handles this automatically.
|
||||
- **Port conflicts after subagent test runs** — a subagent may leave a test process running on the target port. Before enabling the systemd service, check: `ss -tlnp | grep :PORT`. Kill stale processes with `fuser -k PORT/tcp`.
|
||||
- **Subagent timeout for multi-step builds** — the default 600s child timeout may be insufficient for pip installs + service wiring. Either raise delegation.child_timeout_seconds or plan to handle the final 20% (install + enable + test) yourself.
|
||||
Reference in New Issue
Block a user