ishari-mcp-server
by amar-tya
README.md
# ishari-mcp-server
MCP server exposing read-only ISHARI shalawat content (from Supabase) as
a tool an AI agent can call. Built as a learning scaffold following the
official MCP best-practice guidelines (tool naming, Zod validation,
pagination, annotations, RLS-respecting auth).
## Setup
```bash
npm install
cp .env.example .env # then fill in SUPABASE_URL and SUPABASE_ANON_KEY
npm run build
```
## Run locally (stdio)
```bash
npm start
```
It will sit waiting for an MCP client to connect over stdin/stdout - that's normal, it's not a web server.
## Test with MCP Inspector
```bash
npx @modelcontextprotocol/inspector node dist/index.js
```
This opens a local UI where you can call `ishari_search_shalawat` directly
and see the raw request/response, without needing a full AI client.
## Connect to Claude Desktop / Claude Code
Add to your MCP client config (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"ishari": {
"command": "node",
"args": ["/absolute/path/to/ishari-mcp-server/dist/index.js"],
"env": {
"SUPABASE_URL": "https://your-project-ref.supabase.co",
"SUPABASE_ANON_KEY": "your-anon-public-key"
}
}
}
}
```
Restart the client, then ask it something like "cari sholawat tentang rezeki".
## Run remotely over HTTP (e.g. a homelab box + Tailscale)
stdio only works when the client can spawn the server as a local child
process. To run this on a separate machine (a homelab mini PC reachable
over Tailscale) and keep it running continuously, use the HTTP entry point
instead - same tools, same Supabase logic, just a different transport.
**On the remote machine:**
```bash
git clone <this-repo-url> ishari-mcp && cd ishari-mcp
npm install
cp .env.example .env # fill in SUPABASE_URL, SUPABASE_ANON_KEY
npm run build
# Optional but recommended: a shared secret so not everyone on your
# tailnet can call these tools. Generate one and add it to .env:
echo "MCP_API_KEY=$(openssl rand -hex 32)" >> .env
echo "PORT=3000" >> .env
echo "HOST=0.0.0.0" >> .env # 0.0.0.0 so Tailscale peers (not just localhost) can reach it
npm run start:http
```
Visit `http://<tailscale-ip>:3000/mcp` from another device on your tailnet
to confirm it's reachable (a bare GET returns a 405 - that's expected,
it means the server is up and only accepts MCP's POST requests).
**Run it as a systemd service** (so it survives reboots/SSH disconnects) -
create `/etc/systemd/system/ishari-mcp.service`:
```ini
[Unit]
Description=ishari-mcp-server (HTTP)
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/YOUR_USER/ishari-mcp
ExecStart=/usr/bin/node --env-file=.env dist/httpServer.js
Restart=on-failure
User=YOUR_USER
[Install]
WantedBy=multi-user.target
```
Then:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now ishari-mcp
journalctl -u ishari-mcp -f # tail logs
```
**On the client machine**, point it at the URL instead of a local command.
For Claude Code:
```bash
claude mcp add ishari-remote -s user --transport http \
http://<tailscale-ip>:3000/mcp \
--header "Authorization: Bearer <your MCP_API_KEY>"
```
For Claude Desktop (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"ishari-remote": {
"url": "http://<tailscale-ip>:3000/mcp",
"headers": { "Authorization": "Bearer <your MCP_API_KEY>" }
}
}
}
```
**Security notes specific to this mode:**
- Without `MCP_API_KEY` set, anyone who can reach the port (i.e. anyone on
your tailnet, unless restricted by Tailscale ACLs) can call every tool.
Since tools use the Supabase **anon** key (RLS-scoped, same as any
anonymous app user could see), the blast radius is limited - but
`ishari_list_bookmarks` still requires the *caller's own* access_token
per-request regardless, so no amount of API-key bypass exposes another
user's bookmarks.
- This is a stateless HTTP server (`sessionIdGenerator: undefined`) - every
request gets a fresh `McpServer` instance, so there's no session state
that could leak between different callers.
- Prefer Tailscale ACLs restricting which of your own devices can reach
this port, on top of the `MCP_API_KEY` check - defense in depth.
## Current tools
- `ishari_search_shalawat` - search verse translations by keyword, paginated.
- `ishari_get_chapter(book_id, chapter_number)` - fetch a full chapter with all verses.
- `ishari_list_bookmarks(access_token)` - per-user bookmarks, scoped by the
caller's own Supabase Auth token via RLS (see `services/supabaseClient.ts`
for why a fresh client is created per call instead of a shared one).
- `ishari_get_verse_audio(book_id, chapter_number, verse_number?)` - audio
recitation links for a chapter (or one verse), with reciter name and style.
- `ishari_list_hadi(query?)` - list reciters (qari), optionally filtered by
name, each with a count of their audio recitations.
## Evaluating tool-use quality
See `evals/EVALS.md` - 10 prompts to run against a real agent to check it
picks the right tool, extracts arguments correctly, and knows when a request
falls outside what these tools can do.
## Next steps (left as exercises)
- Add a way to search by chapter title, not just translation text (see
eval #5 in `evals/EVALS.md` for why this gap matters).
- `ishari_search_shalawat` currently only searches one `language_code` at a
time - consider whether searching across all languages by default is
better UX.
## Security notes
- Uses the Supabase **anon** key, so every query goes through RLS - this
server can never see more than an anonymous app user legitimately can.
- No secrets are hardcoded; both values come from environment variables.
- Errors from Supabase are surfaced with a hint, not raw internals.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues