Skip to main content
Glama
amar-tya

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.