No description
  • Python 97.6%
  • Shell 1.7%
  • Dockerfile 0.7%
Find a file
Francesco Iannuzzelli 88ef7f328a
All checks were successful
ci / package (push) Successful in 4m6s
ci / notify-mattermost (push) Successful in 3s
Search lists
2026-09-25 15:32:17 +02:00
.forgejo/workflows Initial commit: PeaceLink events MCP server 2026-09-23 18:07:10 +02:00
.gitignore Initial commit: PeaceLink events MCP server 2026-09-23 18:07:10 +02:00
build.sh Initial commit: PeaceLink events MCP server 2026-09-23 18:07:10 +02:00
CLAUDE.md Add keyword graph endpoint 2026-09-23 22:00:57 +02:00
deploy.sh Initial commit: PeaceLink events MCP server 2026-09-23 18:07:10 +02:00
Dockerfile Fix metrics 2026-09-24 14:23:26 +02:00
README.md Capture UA 2026-09-24 14:34:04 +02:00
requirements.txt Initial commit: PeaceLink events MCP server 2026-09-23 18:07:10 +02:00
server.py Search lists 2026-09-25 15:32:17 +02:00

mcp-server — PeaceLink public MCP server

FastMCP server exposing public PeaceLink website data as MCP tools — starting with the event calendar, with more site tools (e.g. search) expected to land here over time. Runs next to the main website (mcp-server.peacelink.it), separate from the ai stack's mcp-peacedata / mcp-guardian servers (those live on the pm83 cluster behind Keycloak auth). This server has no authentication — it wraps data that's already public on the website.

Data source

https://www.peacelink.it/js/events.php — same endpoint the website's own calendar widget uses. This server does not re-implement caching or filtering: it calls that endpoint directly and relies on its existing file-based cache (classes/cache.php, TTL from the cache_ttl ini setting, default 6h) and its province/region ILIKE filtering (classes/events.php, Events::EventsRange() — same province/region convention as classes/assos.php's org search). It only reshapes the JSON into a form that's easier for an LLM to consume (plain-text description, dropped iCal/export noise, grouped map links).

Events are almost entirely in Italy (PeaceLink is an Italian association); a small number are marked online/virtual with no physical location.

Tools

Tool Description
get_upcoming_events(start?, end?, days?, province?, region?) Upcoming events, same params as events.php (start/end as YYYY-MM-DD, days default 28 capped at 90, province/region as Italian names, partial case-insensitive match)

New tools land in this same app (@app.tool() in server.py) unless the surface grows enough to warrant per-domain sub-apps like mcp-peacedata.

Local dev

pip install -r requirements.txt
EVENTS_URL=http://dev.peacelink.it:8070/js/events.php python server.py
# Server on :8003, health check: GET http://localhost:8003/health

Or via the docker compose stack in ../docker:

./build.sh

Env vars

Var Notes
EVENTS_URL Base URL of the events JSON endpoint (default: https://www.peacelink.it/js/events.php)
METRICS_PORT Port for the internal-only Prometheus /metrics server (default 9090)

Discovery

Serves its own .well-known/mcp/server-card.json (unauthenticated) describing the MCP endpoint (https://mcp-server.peacelink.it/mcp) and that no auth is required.

Metrics

/metrics is served on a separate port (METRICS_PORT, default 9090) via prometheus_client.start_http_server(), not on the public app port (8003). This is an internet-facing server, so the public port only ever exposes /health, the discovery manifest, and /mcp — the k8s Service/Ingress forward port 8003 only, and Prometheus scrapes port 9090 directly via the pod's cluster-internal IP (prometheus.io/scrape pod annotations in pck3s/peacelink/mcp_server.yaml), so metrics are unreachable from outside the cluster by construction, not by a path rule.

Request-count/duration labels also bucket unknown paths into "other" (see _KNOWN_PATHS in server.py) so internet scanner noise (/.env, /.git/config, etc.) doesn't blow up metric cardinality.

Which clients/models are using it

Every POST /mcp logs its raw User-Agent (kubectl logs/docker logs, grep for [mcp]) — raw strings aren't put in a metric label since versions/random suffixes would blow up cardinality the same way paths would. For a dashboard-friendly breakdown over time, the mcp_client_requests_total{client=...} counter buckets the same header into a small fixed set of known markers (_CLIENT_MARKERS in server.py) — claude, chatgpt, gemini, perplexity, mcp-remote, or other. Add a marker there when a new client shows up in the logs worth tracking on its own.

Deployment

Runs in the peacelink namespace on the pck3s cluster (not the ai namespace on pm83), alongside the main website. Manifests live in pck3s/peacelink/mcp_server.yaml.

Forgejo Actions (.forgejo/workflows/ci.yml) builds and pushes docker.io/peacelink/mcp-server (latest + short-SHA tags) on every push to main — same pattern as phpeace/pckui. Repo secrets/vars needed on this Forgejo repo: DOCKER_HUB_USER, DOCKER_HUB_TOKEN, and (optionally) MATTERMOST_WEBHOOK_URL.

Once CI has pushed a new image, roll it out:

./deploy.sh