sn-mcp
# sn-mcp
A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an LLM talk to a ServiceNow instance — query records, and (only when you turn it on) write them — without handing the model the keys to the kingdom.
Built for a healthcare context. The bar is: a CISO can read this page and know what the model can and cannot touch.
---
## The problem
LLMs are useful against ServiceNow. They are also one bad prompt away from reading `sys_user`, dumping a table, or following instructions hidden in a work note.
Most MCP wrappers solve “can the model call the API?” They skip “what happens when it tries something it shouldn’t?”
## Why this exists
The day job is regulated healthcare. I needed an agent that could look at a live instance while I was building — and I would not point a raw API token at that instance.
So the product is not “nine ServiceNow tools.” The product is the **safety kernel those tools cannot bypass**.
---
## How a call works
```
LLM
│ stdio (no network, no ports)
▼
sn-mcp
│ 1. Zod validation
│ 2. Table + field allowlist ← default deny
│ 3. Query denylist ← no identity enumeration
│ 4. HTTPS to ServiceNow (10s cap)
│ 5. Error sanitization ← LLM never sees stack traces
│ 6. PHI redaction
│ 7. Response wrapper ← retrieved data is untrusted input
│ 8. Audit log + rate limit
▼
ServiceNow
```
Read tools always go through that path. Write tools add one more gate: **`BUILDER_MODE` is off unless you set it.** Idle sessions cannot create, update, delete, or run scripts.
---
## Design decisions
These are the ones a stranger needs. The rest, including what was rejected, live in [`DECISIONS.md`](./DECISIONS.md).
| Decision | Choice | Why |
|---|---|---|
| Default deny | Only listed tables and fields come back | A miss is a closed door, not an open one |
| Writes are opt-in | `BUILDER_MODE=true` or the write tools refuse | An idle chat cannot change the instance |
| Errors fail safe | Unknown error category = no hint to the model | Better a confused model than a leaked schema |
| PHI regex is not the primary control | Allowlist is primary; redaction is defense in depth | Regex will miss things. We say that out loud. |
| Retrieved data is untrusted | Every SN payload is wrapped and tagged | Stops a work note from becoming an instruction |
| Identity fields are denied everywhere | One `SYSTEM_IDENTITY_FIELDS` list, every table | Closing `caller_id` and leaving `sys_created_by` is not a lock |
What this is **not**: a production connector for a hospital instance. It is built and tested against a developer instance, with Basic auth. OAuth is the gating requirement before it ever sees real PHI.
---
## What it can do
**Always on** (still subject to the kernel):
| Tool | Purpose |
|---|---|
| `query_table` | Query an allowlisted table |
| `get_record` | Fetch one record by `sys_id` |
| `search_kb` | Search published knowledge articles |
| `count_table` | Return a count — no rows cross the boundary |
| `health_check` | Is ServiceNow up, and can we write the audit log? |
**Off until you opt in:**
| Tool | Purpose |
|---|---|
| `create_record` / `update_record` / `delete_record` | Write to the instance |
| `execute_script` | Run server-side JavaScript. Treat it as admin. |
Default allowlist: `incident`, `change_request`, `problem`, `sc_request`, `sc_task`, `kb_knowledge`, `alm_hardware`, `wm_order`. Clinical tables, users, scripts, attachments, and email are denied by name.
---
## Run it
```bash
git clone https://github.com/dadshorts/sn-mcp.git
cd sn-mcp
npm install
cp .env.example .env # set SN_INSTANCE, SN_USERNAME, SN_PASSWORD
npm test # regression suite against the safety kernel
node src/index.mjs # listens on stdio
```
Point your MCP client at `node src/index.mjs`. Credentials stay in `.env` (gitignored). Leave `BUILDER_MODE` unset unless you are sitting down to write.
---
## What's verified
- Safety kernel + write gate exercised on a live developer instance
- Regression suite for allowlist, denylist, wrapping, PHI false-positives, and builder-mode refusals
- Audit log written for every tool call, including refusals
- Months of local operation while building ServiceNow work
This repo is the server. It does not include instance credentials, audit logs, or production data.
TDQS
Scored across 9 tools
Each tool has a clearly distinct purpose: querying records, fetching by sys_id, counting records, creating/updating/deleting, searching the KB, running scripts, and checking health. Even query_table and get_record are distinct (filtered search vs. direct ID lookup), so there is no ambiguity.
Most tools follow a consistent verb_noun snake_case pattern (query_table, create_record, delete_record, etc.). The only minor deviation is 'health_check' which uses a noun-verb compound rather than verb_noun, but it remains readable and consistent in casing.
With 9 tools, the server is well-scoped for its purpose. It covers CRUD, querying, counting, KB search, script execution, and health monitoring without unnecessary bloat or missing essentials.
The tool set provides full record lifecycle coverage (create, read, update, delete), flexible searching with query_table and count_table, knowledge base access, arbitrary script execution, and server health checks. There are no obvious gaps for typical ServiceNow automation workflows.