Skip to main content
Glama
ZivSapir

Customer Support Analyst MCP

by ZivSapir

Customer Support Analyst MCP

Local Model Context Protocol server for natural-language Q&A over a customer-support ticket dataset. Built for analysts and ops - not a customer-facing chatbot.

Model

This MCP server does not call an LLM itself. Natural-language planning is performed by the MCP host. The server is model-agnostic: it uses whatever model the host already has configured.

Tested end-to-end with:

  • Cursor Agent - Composer

Also runnable with Claude Code and Codex (same stdio server; configuration below).

No OPENAI_API_KEY or ANTHROPIC_API_KEY is required by this server. The host's configured model performs planning; this process only executes tools.

How it works

Tool

Use for

ping

Health check / troubleshooting (MCP config smoke test)

get_schema

Compact fields, small filter enums, table counts, and SQL vs search routing

query_tickets

Counts, group-bys, structured filters (read-only SQL; results labeled untrusted)

search_tickets

Lexical keyword/topic examples (BM25); minimal hits + relevance_score (ranking only); subjects labeled untrusted

get_ticket

One ticket by id (detail after search; text marked untrusted)

search_metrics

Lexical FTS match counts / group-bys for free-text queries

Structured counts come from query_tickets (including ticket_tags for label analytics). Match volumes come from search_metrics (lexical FTS only - not semantic topic prevalence). search_tickets hits are ranked examples, not volume - use get_ticket for body/answer. Ticket subject/body/answer are untrusted model input.

get_schema intentionally omits the 1,255-value tag vocabulary to keep first-call context small. Discover relevant tags through an aggregate query_tickets query on ticket_tags (use COUNT(DISTINCT ticket_id) for ticket counts). Wording questions ("mention/say/contain X") use search_metrics; explicit labels use ticket_tags - do not treat one as the other.

FTS uses an inverted index, stemming, and BM25 ranking - better than SQL LIKE for examples, still not paraphrase/embedding search. Multi-word queries default to match_mode: "any" (at least one term); use "all" when every term should be present. Neither mode is exact phrase matching. The dataset is EN+DE; SQL works for both languages. FTS uses DuckDB's default English analyzer, so German text search is best-effort.

Dataset: Tobi-Bueck/customer-support-tickets (Hugging Face Support_Dataset; downloaded once at ingest).

Architecture and rejected alternatives: DECISIONS.md.

Requirements

  • Node.js 20+

  • npm (setup only: install / ingest / build / verify)

  • An MCP host (Cursor, Claude Code, or Codex)

Quick start

Three stages: install and prepare are yours; connect is the host spawning Node.

1. Install

git clone https://github.com/ZivSapir/customer-support-analyst-mcp.git
cd customer-support-analyst-mcp
npm ci

2. Prepare

npm run ingest   # downloads CSV (first run), builds local DuckDB + FTS index
npm run build    # compiles src/ → dist/index.js
npm run verify   # optional: pinned smoke checks

data/ is gitignored. Each machine runs ingest locally against a pinned Hugging Face revision (see src/dataset.ts); the CSV checksum is verified and data/ingest-manifest.json records provenance.

After this step you should have:

  • data/tickets.duckdb

  • dist/index.js (the MCP server entrypoint)

3. Connect (ask questions in the host)

Do not leave npm start running in a terminal to "use" the app. This is a stdio MCP server: Claude Code / Codex / Cursor spawn the process when they need tools:

MCP host (Claude Code / Codex / Cursor)
  → spawns:  node /ABSOLUTE/PATH/TO/customer-support-analyst-mcp/dist/index.js
  → talks over stdin/stdout
  → you ask natural-language questions in the host chat

Next step: pick your host under MCP configuration and paste that JSON/TOML block. Replace /ABSOLUTE/PATH/TO/customer-support-analyst-mcp with the real clone path on your machine (the folder that contains dist/index.js after npm run build). Prefer node + that absolute dist/index.js path (not npm start), so the working directory cannot break startup. Reload/restart MCP in the host, confirm the tools appear, then ask a question in chat.

Optional: npm start is only a manual check that the process boots; it waits for an MCP client on stdin/stdout and is not the normal way to ask questions.

MCP configuration

Replace /ABSOLUTE/PATH/TO/customer-support-analyst-mcp with your clone path (example: /Users/you/code/customer-support-analyst-mcp). The args entry must point at dist/index.js inside that folder. After changing tools, restart/reload the MCP server in the host so it picks up the new tool list.

Cursor

User or project config (.cursor/mcp.json / ~/.cursor/mcp.json):

{
  "mcpServers": {
    "customer-support-analyst": {
      "command": "node",
      "args": ["/ABSOLUTE/PATH/TO/customer-support-analyst-mcp/dist/index.js"]
    }
  }
}

You should see ping, get_schema, query_tickets, search_tickets, get_ticket, and search_metrics. If the server or ping is unavailable, verify the absolute path, build output (dist/index.js), and local ingest (npm run ingest) before debugging dataset queries.

Claude Code

Project-scoped .mcp.json in the repo root:

{
  "mcpServers": {
    "customer-support-analyst": {
      "type": "stdio",
      "command": "node",
      "args": ["/ABSOLUTE/PATH/TO/customer-support-analyst-mcp/dist/index.js"]
    }
  }
}

Or via CLI: claude mcp add --transport stdio customer-support-analyst -- node /ABSOLUTE/PATH/TO/customer-support-analyst-mcp/dist/index.js

Codex

Add to ~/.codex/config.toml (or project-scoped .codex/config.toml in a trusted project):

[mcp_servers.customer-support-analyst]
command = "node"
args = ["/ABSOLUTE/PATH/TO/customer-support-analyst-mcp/dist/index.js"]

Or via CLI: codex mcp add customer-support-analyst -- node /ABSOLUTE/PATH/TO/customer-support-analyst-mcp/dist/index.js

Cursor example file: mcp.config.example.json.

Example questions

Question

Expected tool

How many tickets are in the dataset?

query_tickets

High-priority tickets by queue

query_tickets

Breakdown by language and priority

query_tickets

What are customers saying about refunds?

search_tickets

Password-reset tickets in German (by language column)

query_tickets, or search_tickets with language: "de" and preferably match_mode: "all" (FTS is English-optimized)

How many tickets mention refunds?

search_metrics (lexical wording - not the Refund tag)

High-priority tickets about password resets (examples)

search_tickets with filters; prefer match_mode: "all" for multi-word topics

How many tickets have the Refund tag?

query_tickets on ticket_tags with COUNT(DISTINCT ticket_id)

Optional MCP prompt: ticket-analyst (reminder only - routing lives in tool contracts + get_schema).

Full list of example questions (routing hints for operators - not an automated LLM eval harness): eval/questions.json. npm run verify checks that file's shape and pinned DuckDB/FTS expectations.

Scripts

Script

Purpose

npm run ingest

Pinned CSV → data/tickets.duckdb + FTS + ingest manifest

npm run peek

Print columns + sample rows from local DuckDB

npm run build

Compile src/dist/

npm run verify

Pinned smoke checks (row counts, filters, FTS, SQL guard/FS, eval JSON shape)

npm run dev

Run the stdio server via tsx (development)

npm start

Alias for node dist/index.js - manual stdio boot check only; hosts should spawn Node themselves