ServiceNow MCP Server
README.md
# ServiceNow Developer Actions — MCP Server
An [MCP](https://modelcontextprotocol.io) server that connects Claude Desktop (or any MCP client) to a ServiceNow instance, exposing **guarded developer actions** as natural-language tools. Claude can read incidents, inspect CMDB and table schemas, and create records — with every write isolated to a scoped Update Set, restricted to an allowlist of tables, and protected by a dry-run preview.
Built with **FastMCP**, a typed REST client, and an explicit guardrail layer — the difference between an LLM API wrapper and an agent tool you'd let near a production instance.
---
## Why this project
This demonstrates two skill sets at once:
- **AI engineering / agent architecture** — agentic tool design with safety rails: input schemas (Pydantic), an allowlist + blocklist for writable tables, automatic Update Set scoping, and a `dry_run` mode on risky tools so the model previews intent before mutating anything.
- **ServiceNow integration architecture** — a resilient Table API client with OAuth2 (with Basic-auth fallback), token refresh on 401, and exponential backoff on 429/5xx, plus CMDB and `sys_dictionary` introspection tools.
---
<img width="1249" height="778" alt="20260627-0956-14 5005578" src="https://github.com/user-attachments/assets/955521d6-39b2-4872-8033-c1f5c879fec3" />
## Architecture
```
Claude Desktop / Claude Code
│ (MCP protocol over stdio)
▼
FastMCP Server ── server.py
│
├── tools (read) get_incidents · query_table · query_records · get_record
│ describe_table · get_cmdb_cis · get_update_set
├── tools (write) create_incident · create_script_include · create_record
│ update_record · execute_background_script ← guarded
│
├── guardrails.py allowlist · blocklist · update-set enforcement · dry-run
▼
ServiceNowClient ── client.py (OAuth2 / Basic · retry · backoff · typed errors)
│ REST (Table API + a companion Scripted REST resource for script execution)
▼
ServiceNow instance (PDI)
```
## Tools
| Tool | Type | Notes |
| --------------------------- | ----- | ---------------------------------------------------------|
| `get_incidents` | read | Newest-first, active filter |
| `query_table` | read | GlideRecord-style query on any table |
| `query_records` | read | Like `query_table`, with explicit field selection |
| `get_record` | read | Fetch a single record by `sys_id` |
| `describe_table` | read | Field inspection via `sys_dictionary` |
| `get_cmdb_cis` | read | List CIs from a CMDB class |
| `get_update_set` | read | List changes currently inside an Update Set, for review |
| `create_incident` | write | Allowlisted, Update-Set enforced, `dry_run` |
| `create_script_include` | write | `dry_run=True` by default |
| `create_record` | write | Any allowlisted table (Business Rules, UI Actions, Flows…) |
| `update_record` | write | Patch a field (e.g. `script`) on any allowlisted table |
| `execute_background_script` | write | Preview-only by default; `confirm=True` required to run |
## Guardrails
| Guardrail | What it does | Enforced by |
|---|---|---|
| Writable allowlist | Only dev-artifact tables (`sys_script`, `sys_script_include`, `sys_script_client`, `sys_ui_action`, `sys_ui_page`, `catalog_script_client`, `sys_hub_flow`, `incident`, `sc_req_item`, `task`) may be written. | `assert_writable()` |
| Blocklist wins | Security/identity tables (`sys_user`, `sys_user_group`, `sys_security_acl`, `sys_user_role`, `sys_properties`) are refused even if a table were also allowlisted. | `assert_writable()` (blocklist checked first) |
| Update Set enforcement | Before **any** write, the server resolves `MCP_DEV_UPDATE_SET_ID`, confirms it's a real `in progress` Update Set, makes it the *current* set for the authenticated user (via `sys_user_preference`), and reads that preference back to confirm before proceeding. Unset, not found, not in-progress, or unconfirmed → the write is refused. No write can silently land in Default or in whatever set was last active. | `ensure_update_set()` |
| Dry-run by default | `create_script_include`, `create_record`, `update_record`, and `execute_background_script` default to previewing rather than writing; you opt in per call. | `dry_run` / `confirm` params |
| Background script two-step | `execute_background_script` always returns the script text plus a best-effort static scan of `GlideRecord` table references first. It only executes when called again with `confirm=True` — never automatically. | `execute_background_script()` |
| Confirmation summary | Every successful write returns table, `sys_id`, changed fields, and the Update Set it landed in. | `write_result()` |
### Setting up `execute_background_script`
The Table API has no native endpoint for ad hoc script execution, so this tool POSTs to a small Scripted REST resource you create once on the PDI (Studio → Scripted REST APIs):
```javascript
// Resource: POST /api/x_snc/mcp_dev/execute (path must match SN_SCRIPT_EXEC_PATH)
(function process(request, response) {
var body = request.body.data;
var result;
try {
result = String(eval(body.script));
} catch (e) {
result = "ERROR: " + e.message;
}
return { result: result };
})(request, response);
```
Scope this resource's ACL to an admin-only role — it is equivalent to background script access and should never be broadly exposed.
## Build a Business Rule by chat
1. **"Show me the field name for the incident summary."** → Claude calls `describe_table("incident")` to confirm before writing code against it.
2. **"Create a Business Rule called 'Auto-assign VPN incidents' on `incident`, before insert, that sets `assignment_group` when `short_description` contains 'VPN'. Dry run first."** → Claude calls `create_record("sys_script", {...}, dry_run=True)` and shows you the payload it *would* insert.
3. **"Looks right, go ahead."** → Claude re-calls with `dry_run=False`. `ensure_update_set()` confirms `MCP_DEV_UPDATE_SET_ID` is current, the record is inserted, and you get back table / `sys_id` / fields / Update Set in the response.
4. **"What's in the Update Set so far?"** → Claude calls `get_update_set()` and lists every change recorded, so you can review before promoting or exporting it.
---
## Setup
```bash
git clone <your-repo-url> && cd servicenow-mcp
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
cp .env.example .env # fill in your PDI URL, credentials, and MCP_DEV_UPDATE_SET_ID
pytest # all green
```
### Connect to Claude Desktop
Add to `claude_desktop_config.json` (Developer → Open App Configuration File), then restart:
```json
{
"mcpServers": {
"servicenow": {
"command": "python",
"args": ["-m", "servicenow_mcp.server"],
"cwd": "/absolute/path/to/servicenow-mcp/src",
"env": {
"SN_URL": "https://dev.service-now.com/",
"SN_USER": "user",
"SN_PASS": "pass",
"SN_UPDATE_SET": "MCP Automated Changes",
"MCP_DEV_UPDATE_SET_ID": "sys_id_of_your_dev_update_set"
}
}
}
}
```
A 🔧 indicator confirms the tools loaded. Then try:
> "Show me the 5 newest active incidents"
> "Describe the cmdb_ci_server table"
> "Create an incident: VPN gateway down, urgency high — dry run first"
---
## Tech
Python 3.10+ · FastMCP · Pydantic v2 · requests · pytest + responses
## Roadmap
- OAuth client-credentials grant (service account)
- `commit_to_source_control` tool wrapping Studio's Git integration
- Read-through cache for schema/CMDB lookups
- Structured audit log of every write the server performs
---
_Author: Pavithra Kumaran S — ServiceNow Senior Consultant exploring AI agent architecture._
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues