Skip to main content
Glama
amar-tya

ishari-mcp-server

by amar-tya

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

npm install
cp .env.example .env   # then fill in SUPABASE_URL and SUPABASE_ANON_KEY
npm run build

Related MCP server: Quran Cloud MCP Server

Run locally (stdio)

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

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):

{
  "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:

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

echo "PORT=3000" >> .env
echo "HOST=0.0.0.0" >> .env   # 0.0.0.0 so Tailscale peers (not just localhost) can reach it

# Pick ONE auth mode below (multi-client is recommended if you have more
# than one MCP client connecting - see "Multi-client auth" section).

# Mode A: single shared secret (simplest, one token for everyone)
echo "MCP_API_KEY=$(openssl rand -hex 32)" >> .env

# Mode B: multi-client tokens - see "Multi-client auth" section below
# instead of the line above.

npm run start:http

Multi-client auth (per-client tokens)

If more than one MCP client will connect (e.g. Claude Desktop + Claude Code + another agent), give each one its own revocable token instead of sharing one MCP_API_KEY.

1. Run the migration (creates mcp_clients, RLS enabled, deny-by-default - only the service_role key can read/write it):

create table public.mcp_clients (
  id uuid primary key default gen_random_uuid(),
  client_name text not null,
  token text not null unique,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  last_used_at timestamptz
);

alter table public.mcp_clients enable row level security;

2. Add the service-role key to .env (Project Settings > API > service_role - this is a secret, never commit it):

SUPABASE_SERVICE_ROLE_KEY=your-service-role-secret-key

Setting this env var switches the server into multi-client mode - it takes priority over MCP_API_KEY, and (unlike MCP_API_KEY) there is no "open" fallback: every request must carry a valid token.

3. Insert a token per client (run per client you want to connect):

insert into public.mcp_clients (client_name, token)
values ('my-laptop-claude-code', 'generate-a-random-token-here');

Give each client its own row/token. Give it to the client as its Authorization: Bearer <token> value, same as MCP_API_KEY was used before.

4. Revoke a client by flipping its row:

update public.mcp_clients set is_active = false where client_name = 'my-laptop-claude-code';

Takes effect on the client's next request - no restart needed.

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:

[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:

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:

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):

{
  "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 or SUPABASE_SERVICE_ROLE_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.

  • Multi-client mode's one deviation from the anon-key-only model: checking a bearer token against mcp_clients happens before any caller identity exists, so it can't go through RLS the way every other query in this codebase does. It uses the service_role key instead, in one dedicated file (src/services/supabaseAdminClient.ts) that does nothing else - it's never passed to ishari_* tool logic, and every other Supabase call in the server still goes through the anon key. Treat SUPABASE_SERVICE_ROLE_KEY with the same care as any credential that bypasses RLS project-wide: don't log it, don't commit it, rotate it if it ever leaks.

  • 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 token 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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Provides AI assistants with comprehensive access to Islamic resources including Quran verses with translations, Tafsir commentary, Hadith collections, and audio recitations. Enables users to explore Islamic texts, get daily inspiration, and access scholarly interpretations through natural language queries.
    18
    33 npm
    10
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Connects LLMs to the Quran API (alquran.cloud) to retrieve accurate Quranic text on-demand, reducing hallucinations when working with sensitive religious content.
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to retrieve canonical Quran text, translations, scholarly tafsir, full-text search, and word-level morphology from verified Quran data.
    -
  • A
    license
    A
    quality
    C
    maintenance
    Provides read-only access to Quran data — reciters, surahs, full-text search, ayah retrieval, prayer times, live radio stations, and Hijri dates — so AI agents can answer Islamic content queries. Runs locally via stdio or remotely over stateless HTTP for Claude, ChatGPT, and other MCP clients.
    8
    ISC