freshservice-mcp
by mchluba
README.md
# freshservice-mcp
An [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server for
[Freshservice](https://www.freshservice.com/), letting AI assistants like
Claude work with your service desk.
- **Streamable HTTP transport** — connect any remote-capable MCP client
- **Docker-first** — clone, configure, `docker compose up`, done
- **Configured entirely via environment variables** — nothing tenant-specific in the code
- **Optional bearer-token auth** and an **optional read-only mode**
Built with Python, [FastAPI](https://fastapi.tiangolo.com/) and
[FastMCP](https://gofastmcp.com/). The server grows use case by use case —
tools are added iteratively rather than mirroring the whole Freshservice API.
## Quickstart
Requirements: Docker with the compose plugin.
```bash
git clone <this-repo>
cd freshservice-mcp
cp .env.example .env # then fill in your values
docker compose up -d --build
```
The MCP endpoint is now available at `http://localhost:8000/mcp`.
## Configuration
All settings live in `.env` (see [.env.example](.env.example)):
| Variable | Required | Default | Description |
|---|---|---|---|
| `FRESHSERVICE_DOMAIN` | yes | – | Your tenant, e.g. `yourcompany.freshservice.com` (bare `yourcompany` works too) |
| `FRESHSERVICE_API_KEY` | yes | – | API key of the Freshservice user the server acts as (Profile Settings → API Key). All tool calls run with this user's permissions |
| `FRESHSERVICE_READ_ONLY` | no | `false` | `true` registers no write tools at all — the server can only read |
| `FRESHSERVICE_CREATE_TICKET_SOURCE` | no | `1` | Source code stamped on created tickets (1=Email, 2=Portal, 3=Phone, 9=Walk-up; see `GET /api/v2/ticket_form_fields` for your instance's values, including custom sources) |
| `MCP_AUTH_TOKEN` | no | empty | If set, clients must send `Authorization: Bearer <token>`. Empty = unauthenticated |
| `MCP_PORT` | no | `8000` | Host port the server is published on |
## Connecting a client
**Claude Code:**
```bash
claude mcp add --transport http freshservice http://localhost:8000/mcp \
--header "Authorization: Bearer <your MCP_AUTH_TOKEN>"
```
(omit `--header` if `MCP_AUTH_TOKEN` is empty)
Any other MCP client with Streamable HTTP support works the same way: point
it at `http://<host>:<port>/mcp` and, if configured, send the bearer token.
## Tools
| Tool | Description |
|---|---|
| `find_tickets_by_requester_email` | Find tickets by the requester's email address. By default only open tickets (status Open or Pending); `only_open=false` includes resolved/closed. Paginated, 30 per page |
| `get_ticket` | Full details of a single ticket by ID, including the description text |
| `create_ticket` | Create a ticket on behalf of a requester (by email) — subject + plain-text body, no classification fields, source configurable. Hidden in read-only mode |
| `reply_to_ticket` | Send a reply to the requester on an existing ticket — visible to the requester, notifies them. Hidden in read-only mode |
| `add_internal_note` | Add a private note to a ticket — agents only, requester is not notified, ticket stays unchanged. Hidden in read-only mode |
## Mandatory ticket fields
`create_ticket` deliberately creates tickets **raw** — no category, group or
custom classification fields, just like an incoming e-mail. Your service desk
triages them as usual.
If your instance marks classification fields as *"required for agents when
submitting the form"*, the API will reject such raw creates (HTTP 400
"Validation failed"), because API calls authenticate as an agent. E-mailed
tickets are not affected — that requirement only applies to agents.
The fix is a Freshservice admin setting, not configuration in this server:
1. Go to **Admin → (your workspace) → Service Desk Settings → Field Manager →
Ticket Fields**
2. Edit each affected field (e.g. Category or custom dropdowns)
3. Under the agent behavior, uncheck **"Required when submitting the form"**
and keep **"Required when closing the ticket"** checked
Data quality stays enforced (nobody can *close* a ticket without those
fields), while tickets can come in unclassified through any channel —
including this MCP server.
A dedicated ticket source (e.g. a custom "AI" source, Admin → Field Manager →
Ticket Fields → Source) plus `FRESHSERVICE_CREATE_TICKET_SOURCE` makes
MCP-created tickets easy to recognize and automate on.
## Security notes
- **Set `MCP_AUTH_TOKEN`** whenever the server is reachable beyond localhost
(e.g. `openssl rand -hex 32`). Without it, anyone who can reach the port can
use your Freshservice API key's permissions.
- **Use a least-privilege API key.** The server can never do more than the
Freshservice user behind `FRESHSERVICE_API_KEY` is allowed to.
- **`FRESHSERVICE_READ_ONLY=true`** is a hard switch: write tools
(`create_ticket`, `reply_to_ticket`, `add_internal_note`) are not
registered on the MCP server, so a client cannot even attempt a mutation.
- For public exposure, put the server behind a TLS-terminating reverse proxy.
## Development without Docker
```bash
python -m venv .venv && source .venv/bin/activate
pip install -e .
cp .env.example .env # fill in your values
uvicorn app.main:app --reload
```
## Project structure
```
app/
├── main.py # FastAPI app, mounts the MCP server at /mcp, /health probe
├── config.py # settings from environment variables (pydantic-settings)
├── freshservice.py # async Freshservice API v2 client (auth, 429 retry, errors)
└── tools/
├── __init__.py # central tool registration, enforces read-only mode
├── tickets.py # ticket tools (find, get, create)
└── conversations.py # replies and internal notes on tickets
```
Each new use case becomes a module under `app/tools/` with a
`register(mcp, get_client, settings)` function.
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues