enable-nz-mcp
by liam-edmunds
README.md
# enable-nz-mcp
A public, hosted [MCP](https://modelcontextprotocol.io) server that exposes the content of
[enable.co.nz](https://enable.co.nz) as searchable tools, connectable from Copilot Studio,
VS Code Copilot, Claude Desktop, or any MCP client.
This is a personal portfolio project. It is **not affiliated with Enable New Zealand or any
employer**, and reads only publicly available content from a public website.
- Server: `https://enable-nz-mcp.duckdns.org/mcp` (Streamable HTTP)
- Health check: `https://enable-nz-mcp.duckdns.org/health`
---
## Architecture
```
GitHub Actions (weekly cron)
└─ crawl enable.co.nz → build index.db.new
└─ rsync over SSH → droplet:/opt/enable-nz-mcp/data/index.db.new
└─ atomic mv → index.db
DigitalOcean droplet (Ubuntu 24.04, Sydney)
├─ Caddy → TLS termination + reverse proxy on enable-nz-mcp.duckdns.org
└─ pm2 → node dist/server.js (port 8081, localhost only)
└─ opens a fresh SQLite read connection per query
```
The server never caches the index in memory — every tool call opens a fresh readonly
connection against `index.db` on disk. A weekly crawl rebuilds the index and swaps it in
with a single atomic `mv`, so the index refreshes with zero downtime and no restart.
Read-only in every direction: the crawler only reads enable.co.nz, and the MCP tools only
read the local SQLite index. No write operations of any kind.
---
## Tools
### `search_enable_nz`
Full-text search across enable.co.nz.
| Param | Type | Required | Notes |
|---|---|---|---|
| `query` | string | yes | Search terms |
| `limit` | integer | no | Default 5, max 20 |
| `section` | string | no | Restrict to one section, e.g. `services` |
Returns matching pages ranked by relevance, each with `url`, `title`, `section`, and a
highlighted snippet.
### `get_page`
Retrieve one page's full text by URL.
| Param | Type | Required |
|---|---|---|
| `url` | string | yes |
Returns `url`, `title`, `section`, `description`, `content`, and `crawled_at`. Content over
~15,000 characters is truncated, with `truncated: true` in the response.
### `list_sections`
No input. Lists the site's top-level sections with page counts, and the index's
`crawled_at` timestamp — useful for orienting before a search, or checking how fresh the
data is.
---
## Example queries
Once connected, ask your MCP client things like:
- "Search enable.co.nz for wheelchair funding information."
- "What sections does the enable.co.nz index cover?"
- "Get the full content of https://www.enable.co.nz/services and summarise it."
---
## Connecting an MCP client
### Copilot Studio
1. Open your agent → **Tools** → **Add a tool** → **New tool** → **Model Context Protocol**.
2. **Server name**: `enable-nz-mcp` (or anything you like — this is just a label).
3. **Server URL**: `https://enable-nz-mcp.duckdns.org/mcp`
4. **Authentication**: **No authentication** (leave as-is unless the server has `API_KEY`
set, in which case choose an API key/bearer auth option and supply it).
5. Create → Copilot Studio connects over Streamable HTTP (the only transport it supports
since it dropped SSE in August 2025) and lists `search_enable_nz`, `get_page`, and
`list_sections`. Add the tool to your agent and test with a prompt like "search
enable.co.nz for wheelchair funding".
### VS Code Copilot
1. Open the Command Palette → **MCP: Add Server**.
2. Choose **HTTP** and enter `https://enable-nz-mcp.duckdns.org/mcp`.
3. VS Code will list `search_enable_nz`, `get_page`, and `list_sections` as available tools
once connected.
### Claude Desktop
1. Settings → **Connectors** → **Add custom connector**.
2. URL: `https://enable-nz-mcp.duckdns.org/mcp`.
3. Claude will negotiate the Streamable HTTP transport automatically.
### Any other MCP client
Point it at `https://enable-nz-mcp.duckdns.org/mcp` using the Streamable HTTP transport. If
`API_KEY` is set on the server, send `Authorization: Bearer <key>`.
---
## Local development
```bash
npm ci
cp .env.example .env # edit as needed
npm run crawl # builds data/index.db.new
mv data/index.db.new data/index.db
npm run dev # starts the server with tsx, reading .env
```
Test the running server with [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
```bash
npx @modelcontextprotocol/inspector
# connect to http://127.0.0.1:8081/mcp
```
Or smoke-test by hand:
```bash
curl http://127.0.0.1:8081/health
curl -X POST http://127.0.0.1:8081/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
Run the test suite:
```bash
npm test
```
---
## Deployment
The server runs on an existing DigitalOcean droplet under its own unprivileged system user
(`mcpsvc`), isolated from other services on the same box — `mcpsvc` has no sudo and no read
access outside `/opt/enable-nz-mcp`. See `deploy/ecosystem.config.cjs` for the pm2 process
config and `deploy/Caddyfile.example` for the reverse-proxy block.
Both pm2 and Caddy config changes need root, so `mcpsvc` can go no further than `pm2 save`
on its own — an administrator has to run `pm2 startup` (to persist the process across
reboots) and install/reload the Caddy site block. Rate limiting is not currently applied at
the Caddy layer: it requires the third-party `caddy-ratelimit` plugin, which isn't in this
box's Caddy build.
The `.github/workflows/refresh-index.yml` workflow re-crawls the site weekly (Sunday 02:00
UTC, or manually via `workflow_dispatch`), gates on a minimum page count/size, and deploys
the new index via `rsync` + an atomic `mv` over SSH. It needs three repository secrets:
`DEPLOY_SSH_KEY`, `DEPLOY_HOST`, `DEPLOY_USER` — using a dedicated deploy keypair restricted
via a `command=` directive in `authorized_keys` so it can only run the rsync/mv, not an
arbitrary shell.
---
## Out of scope for v1
- Embeddings / semantic search (keyword search via SQLite FTS5 is sufficient for one
marketing site)
- Headless-browser rendering
- Multi-site support
- Write operations of any kind
- User-level auth or per-tenant isolation
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues