mfa-servicenow-mcp
# MFA ServiceNow MCP
๐ [English](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/README.md) | ๐ฐ๐ท [ํ๊ตญ์ด](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/README.ko.md) | ๐ฏ๐ต [ๆฅๆฌ่ช](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/README.ja.md) | ๐ฎ๐ณ [เคนเคฟเคจเฅเคฆเฅ](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/README.hi.md) | ๐จ๐ณ [็ฎไฝไธญๆ](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/README.zh.md) | ๐ช๐ธ [Espaรฑol](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/README.es.md) | ๐ [**GitHub Pages**](https://jshsakura.github.io/mfa-servicenow-mcp/)
MFA-first ServiceNow MCP server. Authenticates via real browser (Playwright) so Okta, Entra ID, SAML, and any MFA/SSO login just works. Also supports API Key for headless/Docker environments.
[](https://pypi.org/project/mfa-servicenow-mcp/)
[](https://pypi.org/project/mfa-servicenow-mcp/)
[](https://github.com/jshsakura/mfa-servicenow-mcp/actions/workflows/ci.yml)
[](https://ghcr.io/jshsakura/mfa-servicenow-mcp)
[](https://opensource.org/licenses/Apache-2.0)
[](https://jshsakura.github.io/mfa-servicenow-mcp/)
> [!WARNING]
> **Built for personal use โ use at your own risk.** This project was created primarily for the author's own workflows. Risk is actively minimized (read-only defaults, write guards, dry-run previews, and `confirm='approve'` gates on every write), but it operates against **live ServiceNow instances**. You are solely responsible for what it does on your instances. Provided **"as is", without warranty of any kind** (Apache-2.0, see [LICENSE](LICENSE)). Review what a tool will do before approving it.
---
## Table of Contents
- [Features](https://github.com/jshsakura/mfa-servicenow-mcp#features)
- [Setup](https://github.com/jshsakura/mfa-servicenow-mcp#setup)
- [MCP Client Configuration](https://github.com/jshsakura/mfa-servicenow-mcp#mcp-client-configuration)
- [Authentication](https://github.com/jshsakura/mfa-servicenow-mcp#authentication)
- [Tool Packages](https://github.com/jshsakura/mfa-servicenow-mcp#tool-packages)
- [CLI Reference](https://github.com/jshsakura/mfa-servicenow-mcp#cli-reference)
- [Keeping Up to Date](https://github.com/jshsakura/mfa-servicenow-mcp#keeping-up-to-date)
- [Safety Policy](https://github.com/jshsakura/mfa-servicenow-mcp#safety-policy)
- [Performance Optimizations](https://github.com/jshsakura/mfa-servicenow-mcp#performance-optimizations)
- [Local Source Audit](https://github.com/jshsakura/mfa-servicenow-mcp#local-source-audit)
- [Skills](https://github.com/jshsakura/mfa-servicenow-mcp#skills)
- [Docker](https://github.com/jshsakura/mfa-servicenow-mcp#docker)
- [Developer Setup](https://github.com/jshsakura/mfa-servicenow-mcp#developer-setup)
- [Documentation](https://github.com/jshsakura/mfa-servicenow-mcp#documentation)
- [Related Projects](https://github.com/jshsakura/mfa-servicenow-mcp#related-projects-and-acknowledgements)
- [License](https://github.com/jshsakura/mfa-servicenow-mcp#license)
---
## Setup
Two steps: **install**, then **add the server to your MCP client config**. No installer command, no per-client flags.
### 1. Install
The default is **`uvx`** โ no separate install step, it just runs. For most people this is the whole story.
```bash
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows PowerShell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
uvx --refresh --with playwright --from mfa-servicenow-mcp servicenow-mcp --version # fetch + verify the server
uvx --with playwright playwright install chromium # Chromium for MFA/SSO login
```
**To update** โ uvx caches the last version it downloaded and keeps reusing it, so a new release has to be pulled in explicitly with `--refresh`:
```bash
uvx --refresh --with playwright --from mfa-servicenow-mcp servicenow-mcp --version
uvx --with playwright playwright install chromium # a newer Playwright needs a newer Chromium build
```
#### If uvx is blocked โ `pip`
Windows Smart App Control stops uvx from running at all: uvx unpacks an unsigned temporary executable on every run, and SAC blocks it. If uvx suddenly stopped working right after a Windows update, this is almost certainly why. Use pip instead:
```powershell
pip install mfa-servicenow-mcp playwright
python -m playwright install chromium
```
**To update:**
```powershell
pip install --upgrade mfa-servicenow-mcp playwright
python -m playwright install chromium
```
A Python from the [python.org installer](https://www.python.org/downloads/) (signed, 3.10+) passes SAC as-is. Launch it with `python -m servicenow_mcp` rather than the `servicenow-mcp` console script โ that script is an unsigned `.exe` shim pip generates, and SAC blocks it too.
> On mac/Linux the one pip caveat is that Homebrew and distro Pythons refuse global installs under [PEP 668](https://peps.python.org/pep-0668/) (`externally-managed-environment`). Use the python.org installer, or just stay on uvx.
Installing Chromium **up front** matters either way. Deferring it to the first tool call means a ~150 MB download racing the MCP host's handshake deadline, which surfaces as `connection closed`.
> **Guided setup.** Running `servicenow-mcp setup` with no flags (pip: `python -m servicenow_mcp setup`) walks you through numbered menus (pick clients and auth type by number or name โ no free-text guessing), in English or Korean (auto-detected from your locale; force with `SERVICENOW_MCP_LANG=ko|en`).
### 2. Configure your MCP client
Add the server to your client's config file. **The `env` block is identical no matter how you installed** โ only `command`/`args` follow the path you picked above:
| Install | `command` | `args` |
|---|---|---|
| uvx (default) | `uvx` | `["--with","playwright","--from","mfa-servicenow-mcp","servicenow-mcp"]` |
| pip | `python` | `["-m","servicenow_mcp"]` |
Only two env vars are required; `MCP_TOOL_PACKAGE` defaults to `standard`, so leave it out unless you need a different package.
#### Single instance
If you only use one instance, this is all you need.
**Claude Code** โ `.mcp.json` (project root) / `~/.claude.json` (global):
```json
{
"mcpServers": {
"servicenow": {
"command": "uvx",
"args": ["--with", "playwright", "--from", "mfa-servicenow-mcp", "servicenow-mcp"],
"env": {
"SERVICENOW_INSTANCE_URL": "https://your-instance.service-now.com",
"SERVICENOW_AUTH_TYPE": "browser"
}
}
}
}
```
If you installed via pip, swap `command`/`args` to this โ everything else stays the same:
```json
"command": "python",
"args": ["-m", "servicenow_mcp"],
```
**Codex** โ `.codex/config.toml` (project) / `~/.codex/config.toml` (global):
```toml
[mcp_servers.servicenow]
command = "uvx"
args = ["--with", "playwright", "--from", "mfa-servicenow-mcp", "servicenow-mcp"]
# pip: command = "python" / args = ["-m", "servicenow_mcp"]
[mcp_servers.servicenow.env]
SERVICENOW_INSTANCE_URL = "https://your-instance.service-now.com"
SERVICENOW_AUTH_TYPE = "browser"
```
**OpenCode** โ `opencode.json` (project root):
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"servicenow": {
"type": "local",
"command": ["uvx", "--with", "playwright", "--from", "mfa-servicenow-mcp", "servicenow-mcp"],
"enabled": true,
"environment": {
"SERVICENOW_INSTANCE_URL": "https://your-instance.service-now.com",
"SERVICENOW_AUTH_TYPE": "browser"
}
}
}
}
```
Other clients (Cursor, VS Code, Antigravity, Zed, โฆ) and full env options (auth types, tool packages) are in [MCP Client Configuration](https://github.com/jshsakura/mfa-servicenow-mcp#mcp-client-configuration).
Then restart the client. The first browser tool call opens a window for Okta/Entra ID/SAML/MFA login. Sessions persist โ no re-login every time.
#### Multiple instances (dev / test / prod)
If you work across dev / test / prod, **don't stand up several servers.** Changing only `env` lets a single connection handle all of them:
```json
"env": {
"SERVICENOW_ACTIVE_INSTANCE": "dev",
"SERVICENOW_INSTANCE_CONFIG": "{ \"dev\": { \"url\": \"https://acme-dev.service-now.com\", \"auth_type\": \"browser\", \"allow_writes\": true }, \"prod\": { \"url\": \"https://acme.service-now.com\", \"auth_type\": \"browser\" } }"
}
```
All that changes is an alias list taking the place of `SERVICENOW_INSTANCE_URL`; `command`/`args` stay the same. What you get:
- **Production is protected by default** โ an alias without `allow_writes` is read-only. In the example above, `prod` cannot be written to at all.
- **Query another instance without restarting** โ pass `instance` to a read tool, as in `sn_query(instance="prod", ...)`.
- **Compare across instances** โ `compare_instances` puts the same component from dev and prod side by side.
- **One login** โ the browser session is shared across aliases.
The full rules (write routing, gates, `${ENV}` references) live in [Multiple instances โ two approaches](#profiles-vs-multi-process). Only reach for **B. Multi-process** there if the connections have to look separate in your client's UI.
> Prefer an AI to do it? Paste into Claude Code / Cursor / Codex / etc.:
> `Install and configure mfa-servicenow-mcp following https://raw.githubusercontent.com/jshsakura/mfa-servicenow-mcp/main/docs/llm-setup.md`
### If your corporate network blocks the install
TLS-inspecting proxies (Zscaler and friends) and blocked PyPI access have their own path โ see [Install (offline / corporate)](#install-offline--corporate).
---
## Features
- **Browser authentication** for MFA/SSO environments (Okta, Entra ID, SAML, MFA)
- **4 auth modes**: Browser, Basic, OAuth, API Key
- **75 registered tools** with **6 active package profiles** plus disabled `none` โ from minimal read-only to broad bundled CRUD
- **4 workflow skills** with safety gates, sub-agent delegation, and verified pipelines
- **Streamable HTTP transport** โ keep stdio as the default, or expose `/mcp` for HTTP-capable clients and bridges
- **Local source audit** with HTML report, cross-reference graph, dead code detection, and auto-generated domain knowledge
- **Authoritative relationship graphs on disk** โ `_graph.json` (widgetโAngular Provider, from the live M2M) and `_page_graph.json` (pageโwidget, from `sp_instance`) let the LLM answer dependency questions offline instead of re-querying the instance
- **Incremental sync** (`incremental=True`) โ re-download only records changed since last sync (`sys_updated_on` watermark), like `git pull`; `reconcile_deletions=True` flags records deleted on the instance
- **Cross-scope dep auto-resolve** in `download_app_sources` โ pulls global-scope Script Includes, Widgets, Angular Providers, and UI Macros that the app references, so the local bundle is self-contained for analysis
- **Attachment download** (`download_attachment`) โ save a record's attachment file(s) locally and return `saved_path`; successful small downloads also carry a short-lived MCP `ResourceLink`, so isolated clients can explicitly fetch the file as a binary resource without putting base64 in the initial LLM context
- **Excel without boilerplate** (`manage_workbook`) โ list sheets, read rows, regex-find across a tracking workbook; write styled sheets from a plain data spec (house header/border/wrap applied server-side); or fill a COPY of a company form, screenshots embedded. Built for test sign-off and hand-over documents; the form itself is an input and is never written to
- **Dry-run preview** on every write tool (`dry_run=True`) โ returns field-level diff, dependency counts, and precision notes before any side effect. Uses read-only APIs, works under all auth modes.
- Write intent gate: every mutation requires an explicit `confirm='approve'` (accidental-write guardrail, not an adversarial boundary โ see [Safety Policy](#safety-policy))
- Payload safety limits, per-field truncation, and total response budget (200K chars)
- Transient network error retry with backoff
- Tool packages for core, standard, service desk, portal developers, and platform developers โ `full` available for advanced users (see [warning](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/TOOL_PACKAGES.md))
- Developer productivity tools: activity tracking, uncommitted changes, dependency mapping, daily summary
- Full coverage of core ServiceNow artifact tables (see [Supported Tables](https://github.com/jshsakura/mfa-servicenow-mcp#supported-servicenow-tables))
- CI/CD with auto-tagging, PyPI publishing, and Docker multi-platform builds
#### Attachment delivery in isolated environments
`download_attachment` always keeps its existing disk-first behavior. Local agents and containers with a shared volume should read `saved_path`. By default, a successful file up to 10 MiB also returns an MCP `ResourceLink`; a remote client can explicitly call `resources/read` within 15 minutes to receive that exact downloaded file as a typed binary blob. The first tool response never contains the file's base64. Server operators can lower or raise the resource limit with `SERVICENOW_ATTACHMENT_RESOURCE_MAX_MB`, up to a hard maximum of 25 MiB. Links are process-local, are not transferable to another MCP server, and expire when the issuing server restarts. Larger files remain successfully downloaded and the response retains `saved_path`, while explaining that the resource limit was exceeded.
### Supported ServiceNow Tables
| Artifact Type | Table Name | Source Search | Developer Tracking | Safety (Heavy Table) |
|--------------|------------|:---:|:---:|:---:|
| Script Include | `sys_script_include` | โ
| โ
| ๐ก๏ธ |
| Business Rule | `sys_script` | โ
| โ
| ๐ก๏ธ |
| Client Script | `sys_script_client` | โ
| โ
| ๐ก๏ธ |
| Catalog Client Script | `catalog_script_client` | โ
| โฌ | โฌ |
| UI Action | `sys_ui_action` | โ
| โ
| ๐ก๏ธ |
| UI Script | `sys_ui_script` | โ
| โ
| ๐ก๏ธ |
| UI Page | `sys_ui_page` | โ
| โ
| ๐ก๏ธ |
| UI Macro | `sys_ui_macro` | โ
| โฌ | ๐ก๏ธ |
| Scripted REST API | `sys_ws_operation` | โ
| โ
| ๐ก๏ธ |
| Fix Script | `sys_script_fix` | โ
| โ
| ๐ก๏ธ |
| Scheduled Job | `sysauto_script` | โ
| โฌ | โฌ |
| Script Action | `sysevent_script_action` | โ
| โฌ | โฌ |
| Email Notification | `sysevent_email_action` | โ
| โฌ | โฌ |
| ACL | `sys_security_acl` | โ
| โฌ | โฌ |
| Transform Script | `sys_transform_script` | โ
| โฌ | โฌ |
| Processor | `sys_processor` | โ
| โฌ | โฌ |
| Service Portal Widget | `sp_widget` | โ
| โ
| ๐ก๏ธ |
| Angular Provider | `sp_angular_provider` | โ
| โ
| โฌ |
| Portal Header/Footer | `sp_header_footer` | โ
| โฌ | โฌ |
| Portal CSS | `sp_css` | โ
| โฌ | โฌ |
| Angular Template | `sp_ng_template` | โ
| โฌ | โฌ |
| Metadata / XML Definitions | `sys_metadata` | โ
| โฌ | ๐ก๏ธ |
| Update XML | `sys_update_xml` | โ
| โฌ | โฌ |
---
## Install (offline / corporate)
For most users the [Setup](https://github.com/jshsakura/mfa-servicenow-mcp#setup) above (uvx) is all you need. Two corporate-network cases are worth calling out.
The common case is **PyPI reachable, but HTTPS is TLS-inspected** (Zscaler / Netskope / corporate MITM) โ that is what the section below covers.
If PyPI itself is blocked outright, neither uvx nor pip can reach the package. Ask your IT team to allowlist `pypi.org` and `files.pythonhosted.org`, or to mirror the package on an internal index you can point `pip install --index-url` at.
### Installing behind a TLS-inspecting proxy (Zscaler etc.)
Use this when PyPI **is** reachable but a TLS-inspecting proxy re-signs HTTPS, so installs and runtime calls fail with `SSL: CERTIFICATE_VERIFY_FAILED`. Registering the proxy's root CA in the **OS trust store is not enough** โ Python (`pip`, `requests`, `httpx`), `curl_cffi`, and Playwright each ship their own CA bundle (certifi / libcurl / node) and ignore the OS store unless you point them at the cert via env.
**1. Get the proxy root CA** as a PEM file (ask IT, or export it from the OS keychain). Assume it lands at `/etc/ssl/zscaler-root.pem` (Windows: `C:\certs\zscaler-root.pem`).
**2. Install** โ point the installer at the cert:
```bash
pip install --cert /etc/ssl/zscaler-root.pem mfa-servicenow-mcp
python -m playwright install chromium # NODE_EXTRA_CA_CERTS (step 3) covers its download
```
Prefer uvx? `uv` can use the OS trust store directly (where the proxy CA is already registered):
```bash
UV_NATIVE_TLS=1 uvx --with playwright --from mfa-servicenow-mcp servicenow-mcp --version
```
**3. Runtime โ set the CA path in your MCP client `env`.** The non-obvious part: live ServiceNow calls go through **curl_cffi (libcurl)**, which reads `CURL_CA_BUNDLE` โ *not* `REQUESTS_CA_BUNDLE`. Set all of them so every layer trusts the proxy:
```json
{
"mcpServers": {
"servicenow": {
"command": "python",
"args": ["-m", "servicenow_mcp"],
"env": {
"SERVICENOW_INSTANCE_URL": "https://your-instance.service-now.com",
"SERVICENOW_AUTH_TYPE": "browser",
"CURL_CA_BUNDLE": "/etc/ssl/zscaler-root.pem",
"REQUESTS_CA_BUNDLE": "/etc/ssl/zscaler-root.pem",
"SSL_CERT_FILE": "/etc/ssl/zscaler-root.pem",
"NODE_EXTRA_CA_CERTS": "/etc/ssl/zscaler-root.pem"
}
}
}
}
```
| Env var | Layer it fixes |
|---------|----------------|
| `CURL_CA_BUNDLE` | **curl_cffi / libcurl โ the actual ServiceNow API + browser-login probe calls** |
| `REQUESTS_CA_BUNDLE` | `requests` (OAuth / API-key token calls, fallback HTTP path) |
| `SSL_CERT_FILE` | Python stdlib `ssl` / `httpx` / `uv` |
| `NODE_EXTRA_CA_CERTS` | Playwright's Chromium download |
| `PIP_CERT` (install only) | `pip` fetching from PyPI (same as `--cert`) |
In a fully-inspected network the proxy re-signs every host, so the single proxy-root PEM covers all HTTPS. If some hosts **bypass** the proxy, concatenate the proxy root with certifi's bundle (`python -m certifi` prints its path) into one PEM and point the env vars at that.
> Last resort if you genuinely can't obtain the PEM: `pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org mfa-servicenow-mcp` skips verification for the **install only** โ it does nothing for runtime ServiceNow calls, which still need `CURL_CA_BUNDLE`. Prefer the cert path; `--trusted-host` disables a security control.
## MCP Client Configuration
> Recommended: use [Setup](https://github.com/jshsakura/mfa-servicenow-mcp#setup) above. Use the copy-paste configs below when you need to inspect, repair, or hand-manage a client config file.
Each project can connect to a different ServiceNow instance. Set the config in your **project directory** so each project has its own instance URL and credentials.
| Client | Project Config | Global Config | Format |
|--------|---------------|--------------|--------|
| Claude Code | `.mcp.json` | `~/.claude.json` | JSON |
| Cursor | `.cursor/mcp.json` | *Project only* | JSON |
| VS Code (Copilot) | `.vscode/mcp.json` | *Project only* | JSON |
| Zed | *Global only* | `~/.config/zed/settings.json` | JSON |
| OpenAI Codex | `.codex/config.toml` | `~/.codex/config.toml` | TOML |
| OpenCode | `opencode.json` | *Project only* | JSON |
| Windsurf | *Global only* | `~/.codeium/windsurf/mcp_config.json` | JSON |
| Claude Desktop | *Global only* | `claude_desktop_config.json` | JSON |
| AntiGravity | *Global only* | `~/.gemini/antigravity/mcp_config.json` | JSON |
| Docker | *Env vars only* | *Env vars only* | Env vars |
Copy-paste configs for each client: **[Client Setup Guide](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/CLIENT_SETUP.md)**
> `SERVICENOW_USERNAME` / `SERVICENOW_PASSWORD` are optional โ they prefill the MFA login form. On Windows, set these as system environment variables.
#### Profiles vs. multi-process
The examples above are single-instance โ that stays the default. With more than one instance there are two ways to go, and it's worth **picking one before you configure anything:**
| | **A. Profiles** (recommended) | **B. Multi-process** |
|---|---|---|
| Server processes | 1 | one per instance |
| Connections the client sees | 1 | 3 |
| Choosing an instance | `instance="test"` per call | pinned to the process |
| Cross-instance comparison | **works** (`compare_instances`) | impossible โ processes don't know each other |
| Browser login | one shared session | one login per process |
| Write control | `allow_writes` per alias | per-process config |
**Most people want A.** Write safety is already solved there โ leave `allow_writes` off a prod alias and it's read-only, and writes to a non-active instance have to clear the `confirm_instance` gate. On top of that, only A gives you cross-instance comparison and a single login.
**Pick B only when the connections need to be visibly separate in the client UI.** Tool names come through as `mcp_snow-prd_*`, so a human tells them apart at a glance. The cost is three logins, no comparison, and three copies of the config. Details: [Telling multiple connections apart](#telling-multiple-connections-apart---server-name).
##### A. Profiles
To switch between several instances from one client, list them in `SERVICENOW_INSTANCE_CONFIG` (alias โ settings) and pick the active one with `SERVICENOW_ACTIVE_INSTANCE`. Each alias can carry its **own credentials** (`username` / `password` / `auth_type` / `api_key`); `${ENV}` references keep secrets out of the JSON. The single-instance `SERVICENOW_INSTANCE_URL` form still works as a fallback.
```json
{
"mcpServers": {
"servicenow": {
"command": "uvx",
"args": ["--with", "playwright", "--from", "mfa-servicenow-mcp", "servicenow-mcp"],
"env": {
"MCP_TOOL_PACKAGE": "standard",
"SERVICENOW_ACTIVE_INSTANCE": "dev",
"SERVICENOW_INSTANCE_CONFIG": "{ \"dev\": { \"url\": \"https://acme-dev.service-now.com\", \"auth_type\": \"browser\", \"username\": \"dev_user\", \"password\": \"${SERVICENOW_DEV_PASSWORD}\", \"allow_writes\": true }, \"test\": { \"url\": \"https://acme-test.service-now.com\", \"auth_type\": \"browser\", \"username\": \"test_user\", \"password\": \"${SERVICENOW_TEST_PASSWORD}\" } }"
}
}
}
}
```
`SERVICENOW_ACTIVE_INSTANCE` is where writes default; read tools peek at the others with `instance="test"`, and a single write can be routed to a non-active instance with `instance="test" confirm_instance="test" confirm="approve"` (guarded, and verified after it lands). Full rules (write routing, gating, comparison, `${ENV}`): [Multi-Instance Mode](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/README.md#multi-instance-mode-comparison--guarded-single-call-writes).
##### B. Multi-process
Only worth it when you want the connections split apart in the client UI. Each entry **pins one instance** and gets its own name via `--server-name`:
```json
{
"mcpServers": {
"snow-dev": {
"command": "uvx",
"args": ["--with", "playwright", "--from", "mfa-servicenow-mcp", "servicenow-mcp", "--server-name", "snow-dev"],
"env": {
"SERVICENOW_INSTANCE_URL": "https://acme-dev.service-now.com",
"SERVICENOW_AUTH_TYPE": "browser"
}
},
"snow-prd": {
"command": "uvx",
"args": ["--with", "playwright", "--from", "mfa-servicenow-mcp", "servicenow-mcp", "--server-name", "snow-prd"],
"env": {
"SERVICENOW_INSTANCE_URL": "https://acme.service-now.com",
"SERVICENOW_AUTH_TYPE": "browser",
"MCP_TOOL_PACKAGE": "standard"
}
}
}
}
```
Tool names are then pinned to `mcp_snow-dev_*` / `mcp_snow-prd_*`. Drop `--server-name` and both advertise themselves as `ServiceNow`, so the client numbers them by load order (`mcp_servicenow`, `mcp_servicenow2`) โ and that numbering can shift between restarts, which means you can never trust which one is production.
To keep the production connection read-only, give it a read-only `MCP_TOOL_PACKAGE`. Unlike A, there is no `allow_writes` alias gate here โ **the tool package is the only thing blocking writes.**
> Login prompts come up per process, and cross-instance tools like `compare_instances` are unavailable โ each process only knows its own instance. If that bites, go with A.
---
## Authentication
Choose the auth mode based on your ServiceNow environment.
### Browser Auth (MFA/SSO) โ Default
The [Setup](https://github.com/jshsakura/mfa-servicenow-mcp#setup) command uses browser auth by default. Optional flags:
| Flag | Env Variable | Default | Description |
|------|-------------|---------|-------------|
| `--browser-username` | `SERVICENOW_USERNAME` | โ | Prefill login form username |
| `--browser-password` | `SERVICENOW_PASSWORD` | โ | Prefill login form password |
| `--browser-headless` | `SERVICENOW_BROWSER_HEADLESS` | `false` | Run browser without GUI |
| `--browser-timeout` | `SERVICENOW_BROWSER_TIMEOUT` | `120` | Login timeout in seconds |
| `--browser-session-ttl` | `SERVICENOW_BROWSER_SESSION_TTL` | `30` | Session TTL in minutes |
| `--browser-user-data-dir` | `SERVICENOW_BROWSER_USER_DATA_DIR` | โ | Override the Chromium profile path. Rarely needed โ see the sandbox note below before setting it. |
| `--browser-probe-path` | `SERVICENOW_BROWSER_PROBE_PATH` | user-specific `sys_user` lookup when a username is known, otherwise `/api/now/table/sys_user_preference?sysparm_limit=1&sysparm_fields=sys_id` | Session validation endpoint (avoids 401 on non-admin sessions) |
| `--browser-login-url` | `SERVICENOW_BROWSER_LOGIN_URL` | โ | Custom login page URL |
#### Login sharing across hosts and instances โ how it actually works
The server caches two things under `~/.mfa_servicenow_mcp/`: the Playwright profile (Chromium SSO cookies) and a session JSON (parsed cookies reused on the next start). Both are **scoped per instance + username** โ files are named `profile_<host>_<user>` and `session_<host>_<user>.json`.
That scoping does two things for you automatically, with **no configuration**:
- **Multiple hosts share one login.** Claude Code and Codex on the same machine both resolve `~/.mfa_servicenow_mcp/`, so whichever logs in first writes the session and the other reuses it โ no second MFA prompt.
- **Different instances / different credentials stay isolated.** Each instance+user gets its own profile and session file, so dev and test (or two accounts) never collide. For multiple instances, configure them in `SERVICENOW_INSTANCE_CONFIG` (JSON) โ each alias gets its own scoped cache; you do **not** manage this with a profile path.
**Do not set `SERVICENOW_BROWSER_USER_DATA_DIR` to "share" logins.** It overrides the profile path verbatim โ the per-instance scoping is bypassed, so every instance you run is forced into one Chromium profile and their cookies collide. The only legitimate use is a narrow one: a **sandboxed** host (e.g. Claude Desktop on macOS) that remaps `HOME` to a container path, so its `~/.mfa_servicenow_mcp/` no longer matches the terminal's. In that single-instance case, point the sandboxed host at the real home path:
```bash
# Only when a sandbox remapped HOME, and only for a single-instance host
export SERVICENOW_BROWSER_USER_DATA_DIR="/Users/you/.mfa_servicenow_mcp/profile_acme"
```
If you run more than one instance, leave this unset and let the per-instance scoping do its job.
### Basic Auth
Use this for PDIs or instances without MFA.
```bash
python -m servicenow_mcp \
--instance-url "https://your-instance.service-now.com" \
--auth-type "basic" \
--username "your_id" \
--password "your_password"
```
### OAuth
Current CLI support expects OAuth password grant inputs.
```bash
python -m servicenow_mcp \
--instance-url "https://your-instance.service-now.com" \
--auth-type "oauth" \
--client-id "your_client_id" \
--client-secret "your_client_secret" \
--username "your_id" \
--password "your_password"
```
If `--token-url` is omitted, the server defaults to `https://<instance>/oauth_token.do`.
### API Key
```bash
python -m servicenow_mcp \
--instance-url "https://your-instance.service-now.com" \
--auth-type "api_key" \
--api-key "your_api_key"
```
Default header: `X-ServiceNow-API-Key` (customizable with `--api-key-header`).
---
## Tool Packages
`MCP_TOOL_PACKAGE` controls which tools the server exposes. **Default: `standard`** โ no config needed for most users.
> [!WARNING]
> **Any package above `standard` grants write access and is an advanced option.** `service_desk`, `portal_developer`, `platform_developer`, and `full` all let an AI agent create, update, and delete records โ `full` does so across every domain at once. Most users should stay on the read-only default `standard` and only opt up to the narrowest write package their task actually requires.
Read-only (safe defaults):
| Package | Tools | ~Tokens | Description |
| :--- | :---: | :---: | :--- |
| `none` | 0 | 0 | Disabled profile for intentionally turning tools off |
| `core` | 12 | ~3.0K | Minimal read-only essentials for health, schema, discovery, and key artifact lookups |
| `standard` | 31 | ~7.3K | **(Default)** Read-only across incidents, changes, portal, logs, and source analysis |
โ ๏ธ Write-capable (advanced โ grants create/update/delete):
| Package | Tools | ~Tokens | Description |
| :--- | :---: | :---: | :--- |
| `service_desk` | 33 | ~8.2K | โ ๏ธ standard + incident and change operational writes |
| `portal_developer` | 50 | ~10.6K | โ ๏ธ standard + portal, changeset, script include, and local-sync delivery writes |
| `platform_developer` | 44 | ~10.8K | โ ๏ธ standard + workflow, Flow Designer, UI policy, incident/change, and script writes |
| `full` | 61 | ~13.8K | โ ๏ธ **Most advanced** โ all write tools across all domains at once |
> **~Tokens** is the approximate footprint each package's tool schemas add to the model's context per request (measured with tiktoken `cl100k_base` over the server's compacted schemas; actual Claude counts vary slightly). Staying on the narrowest package keeps the context budget โ and cost โ down.
Each server process binds to one active ServiceNow instance for ordinary tools. A write to a *different* configured instance is possible per call, but only through an explicit, guarded acknowledgement (below) โ never a silent switch.
### Multi-Instance Mode (comparison + guarded single-call writes)
When you need to compare dev/test/prod or deploy to a chosen one, opt into named instances with `SERVICENOW_INSTANCE_CONFIG`. `SERVICENOW_ACTIVE_INSTANCE` is still required.
Two things are global, one is per-instance:
- **Tool surface is global** โ set once with `MCP_TOOL_PACKAGE`. Only one instance is ever active per server process, so there is no per-instance tool package.
- **Write permission is per-instance** โ each alias carries `allow_writes`. It is enforced at call time against the active instance: a write tool can be loaded but still refused if the active instance has `allow_writes: false`. Writes are opt-in: omit `allow_writes` and the instance is read-only.
- **Credentials are per-instance with global fallback** โ put `username` / `password` / `api_key` (and `auth_type`) on an alias to override; omit them and the alias inherits the global `SERVICENOW_USERNAME` / `SERVICENOW_PASSWORD` / etc. So if every instance shares one login, set it once globally and leave the alias entries credential-free.
Other rules:
- **Read tools accept an `instance` argument** to run a single read against a non-active instance โ e.g. `sn_query(instance="test", table="incident", ...)` or `sn_health(instance="test")` while `dev` stays active. Every read tool in your package exposes it in its schema (enum of configured aliases). This is how you peek at another instance's data without restarting.
- **A single write can be routed to a non-active instance**, but never silently. Pass `instance="test" confirm_instance="test" confirm="approve"` (target named twice โ as intent and acknowledgement) and the target must have `allow_writes=true`. Only that one write goes there; the active instance is restored immediately after. A target/confirm mismatch or a read-only target is refused with an explicit message, so a dev/test/prod mix-up cannot land on the wrong instance. The write is then re-read on the target and reported as `landed` (or `WRITE_NOT_LANDED`), with `target_instance` echoed โ "success" means the content is confirmed present on the intended instance, not just a 200.
- `list_instances` reports configured aliases plus the active one and each one's write flag. `compare_instances` performs read-only table comparisons across aliases.
- Switching the *default* active instance requires restarting the MCP client โ it is read once at server startup, not refreshed live. (Per-call `instance=` routing above does not need a restart.)
Example โ shared global login, per-instance write gating:
```bash
export MCP_TOOL_PACKAGE=standard
export SERVICENOW_USERNAME=svc_account
export SERVICENOW_PASSWORD='...'
export SERVICENOW_ACTIVE_INSTANCE=dev
export SERVICENOW_INSTANCE_CONFIG='{
"dev": { "url": "https://acme-dev.service-now.com", "allow_writes": true },
"test": { "url": "https://acme-test.service-now.com", "allow_writes": true },
"prod": { "url": "https://acme-prod.service-now.com", "allow_writes": false }
}'
```
To give an instance its own login instead, add the fields to that alias (a `${ENV}` reference is resolved, so you can keep secrets out of the JSON):
```json
"prod": { "url": "https://acme.service-now.com", "username": "prod_user", "password": "${SERVICENOW_PROD_PASSWORD}" }
```
Use `compare_instances` for dev/test drift checks. For promoting MANY records (especially Service Portal / scoped tables), prefer an Update Set (commit on source, retrieve + commit on target in the UI) over per-record cross-instance writes โ it bypasses the per-table/SP ACLs that single Table-API writes hit.
If a tool is not available in your current package, the server tells you which package includes it.
For the full reference (all packages, inheritance details, config syntax): [Tool Packages Advanced Guide](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/TOOL_PACKAGES.md).
---
## CLI Reference
### Server Options
| Flag | Env Variable | Default | Description |
|------|-------------|---------|-------------|
| `--instance-url` | `SERVICENOW_INSTANCE_URL` | *required* | ServiceNow instance URL |
| `--auth-type` | `SERVICENOW_AUTH_TYPE` | `basic` | Auth mode: `basic`, `oauth`, `api_key`, `browser` |
| `--tool-package` | `MCP_TOOL_PACKAGE` | `standard` | Tool package to load |
| `--server-name` | `SERVICENOW_MCP_SERVER_NAME` | `ServiceNow` | MCP server name advertised to the client |
| `--transport` | `SERVICENOW_MCP_TRANSPORT` | `stdio` | MCP transport: `stdio` or `http` |
| `--http-host` | `SERVICENOW_MCP_HTTP_HOST` | `127.0.0.1` | Host for `--transport http` |
| `--http-port` | `SERVICENOW_MCP_HTTP_PORT` | `8000` | Port for `--transport http` |
| `--http-path` | `SERVICENOW_MCP_HTTP_PATH` | `/mcp` | Streamable HTTP endpoint path |
| `--http-allowed-hosts` | `SERVICENOW_MCP_HTTP_ALLOWED_HOSTS` | loopback hosts | Comma-separated Host allowlist for DNS rebinding protection |
| `--http-disable-dns-rebinding-protection` | `SERVICENOW_MCP_HTTP_DISABLE_DNS_REBINDING_PROTECTION` | `false` | Disable DNS rebinding protection behind trusted network controls |
| `--http-json-response` | `SERVICENOW_MCP_HTTP_JSON_RESPONSE` | `false` | Return JSON responses instead of SSE streams |
| `--timeout` | `SERVICENOW_TIMEOUT` | `30` | HTTP request timeout (seconds) |
| `--debug` | `SERVICENOW_DEBUG` | `false` | Enable debug logging |
HTTP transport example:
```bash
servicenow-mcp --transport http --http-host 127.0.0.1 --http-port 8000
```
The MCP endpoint is `http://127.0.0.1:8000/mcp`; `/health` returns a lightweight health response.
#### Telling multiple connections apart (`--server-name`)
If you register **several server entries** in one client (dev / stg / prd as separate processes), they all default to the name `ServiceNow`, so the client disambiguates them by load order โ `mcp_servicenow`, `mcp_servicenow2`, `mcp_servicenow3`. That numbering can change between restarts, which makes it **untrustworthy for telling which one is production.** Name each connection instead:
```bash
servicenow-mcp --server-name snow-prd # uvx / console script
python -m servicenow_mcp --server-name snow-prd # pip
```
The tool namespace is then pinned to `mcp_snow-prd_*`. `SERVICENOW_MCP_SERVER_NAME` does the same thing as an env var, and the flag wins if both are set. Unset, it stays `ServiceNow`, so existing configs keep working.
> Looking to switch instances inside **one** server instead? That's [Multi-Instance Mode](#multi-instance-mode-comparison--guarded-single-call-writes), not this. The two are unrelated โ `--server-name` is the name the client sees, while a multi-instance alias names an instance inside a single process.
### Basic Auth
| Flag | Env Variable |
|------|-------------|
| `--username` | `SERVICENOW_USERNAME` |
| `--password` | `SERVICENOW_PASSWORD` |
### OAuth
| Flag | Env Variable |
|------|-------------|
| `--client-id` | `SERVICENOW_CLIENT_ID` |
| `--client-secret` | `SERVICENOW_CLIENT_SECRET` |
| `--token-url` | `SERVICENOW_TOKEN_URL` |
| `--username` | `SERVICENOW_USERNAME` |
| `--password` | `SERVICENOW_PASSWORD` |
### API Key
| Flag | Env Variable | Default |
|------|-------------|---------|
| `--api-key` | `SERVICENOW_API_KEY` | โ |
| `--api-key-header` | `SERVICENOW_API_KEY_HEADER` | `X-ServiceNow-API-Key` |
### Script Execution
| Flag | Env Variable |
|------|-------------|
| `--script-execution-api-resource-path` | `SCRIPT_EXECUTION_API_RESOURCE_PATH` |
---
## Keeping Up to Date
Pick the one matching how you installed (the same commands are in the [Install section](#1-install)):
```bash
# uvx โ it caches the last version it downloaded, so --refresh is how you pull a new one
uvx --refresh --with playwright --from mfa-servicenow-mcp servicenow-mcp --version
uvx --with playwright playwright install chromium
```
```powershell
# pip
pip install --upgrade mfa-servicenow-mcp playwright
python -m playwright install chromium
```
Chromium gets refreshed alongside in both cases because a newer Playwright wants a different Chromium build (see below).
After refreshing, **restart your MCP client** (Claude Code, Cursor, etc.) to load the new version.
Check the current version:
```bash
uvx --from mfa-servicenow-mcp servicenow-mcp --version # uvx
python -m servicenow_mcp --version # pip
```
### Why Chromium has to be installed up front
A new Playwright release wants a different Chromium build. Left alone, the *first* browser tool call has to fetch ~150 MB of browser binaries โ which on a slow link blows past the MCP host's handshake timeout and surfaces as:
```text
MCP startup failed: handshaking with MCP server failed: connection closed: initialize response
```
That's why the upgrade commands above run `playwright install chromium` every time.
> **Why we no longer auto-install Chromium inside the MCP server:** that download used to run during the first tool call. On a slow link the subprocess outlived the host's handshake deadline and the client reported "connection closed". v1.13.1 changed this โ the MCP server now only *warns* if Chromium is missing. Install it ahead of time (out-of-band, no handshake timer).
---
## Safety Policy
All mutating tools require an explicit `confirm='approve'` argument.
Rules:
1. Mutating tools with prefixes such as `create_`, `update_`, `delete_`, `remove_`, `add_`, `move_`, `activate_`, `deactivate_`, `commit_`, `publish_`, `submit_`, `approve_`, `reject_`, `resolve_`, `reorder_`, and `execute_` require confirmation.
2. You must pass `confirm='approve'`.
3. Without that parameter, the server rejects the request before execution.
This policy applies regardless of the selected tool package.
> **What this gate is โ and is not.** The server enforces `confirm='approve'`
> before any write, but the argument is supplied by the **same LLM** that issued
> the call. So the gate is an **intent checkpoint that stops accidental or
> ambiguous mutations** โ it forces a deliberate, auditable "yes" and a preview
> hint. It is **not** a defense against a determined or prompt-injected agent,
> which can simply include `confirm='approve'`. Treat it as guardrails, not a
> security boundary: review what a tool will do before approving, run against a
> **least-privilege** ServiceNow account, and do not rely on confirmation alone
> in adversarial settings. For high-stakes automation, add out-of-band approval.
### Write Guards
Beyond the confirm gate, every write runs through deterministic guards that block unsafe writes *before* they reach ServiceNow. The concurrent-edit and duplicate-create checks run **after** the confirm gate, so an unconfirmed write never touches the network. Each guard fails **open** on a denied/failed pre-read โ it never blocks a legitimate write just because it couldn't look first. The intent is simple: **you should never be able to silently clobber a teammate's change** โ if someone else touched the record, the write stops and tells you, rather than overwriting and moving on.
> **Fail-open vs fail-closed.** The default (fail-open) favors availability: if the pre-write audit read cannot run (network error, ACL denial, 5xx), the write proceeds. That means the concurrent-edit guard can silently no-op exactly when the instance is unreachable. Security-sensitive deployments can set `SERVICENOW_WRITE_GUARDS_FAIL=closed` so a guard that **could not verify** blocks instead โ trading availability for the guarantee that a lost-update check never silently passes. Scoped to genuine read *failures*; a successful read that finds no conflict still proceeds.
| Guard | Protects against | Override / toggle |
|---|---|---|
| Concurrent edit (G3/G8) | Blindly overwriting a record a **different user** edited within the last 10 min. Covers `sn_write`, `manage_portal_component`, and the `manage_*` update tools โ including `manage_script_include`, `manage_flow_designer`, `manage_workflow`, `manage_kb_article`, `manage_portal_layout`, and `manage_widget_dependency`. Decided by a **live remote read** of `sys_updated_by`/`sys_updated_on` โ never the local copy. | `SERVICENOW_CONCURRENT_EDIT_GUARD=off`; window via `SERVICENOW_CONCURRENT_EDIT_WINDOW_MIN` (default `10`); fail-closed via `SERVICENOW_WRITE_GUARDS_FAIL=closed` |
| Source push drift (live anchor + update-set HOLD) | Pushing edited source back with `update_remote_from_local` adds two checks the time-window can't catch: a **time-independent** compare of the remote's current `sys_updated_on` against the value recorded at download (catches an overwrite hours or **days** later), and a live check for the record being **held in another user's uncommitted update set**. | `force=true` to push past a detected drift |
| Duplicate create (G9) | Silently creating a second record with a name that already exists, on tables ServiceNow does not make unique (`sys_update_set`, `wf_workflow`, `sys_user_group`, `sys_user`). | pass `allow_duplicate='true'` to create anyway |
| Flow Designer raw write (G6) | Raw `sn_write` to `sys_hub_*` tables that corrupt flow snapshots โ forces `manage_flow_designer`. | โ |
| Publish-class (G7) | Accidental publish/commit/push โ needs a second `confirm_publish='approve'`. | โ |
| Cross-instance push | Pushing local source downloaded from instance A into instance B (origin read from `_settings.json` / `_manifest.json`). | re-download from the correct instance |
Disable the whole layer with `SERVICENOW_WRITE_GUARDS=off`. In multi-instance mode, every write response also carries an `instance_target` field (and reads routed elsewhere an `instance_source`) so the instance a call hit is always visible.
### Portal Investigation Safety
Portal investigation tools are conservative by default:
- `search_portal_regex_matches` starts with widget-only scanning, linked expansion off, and small default limits.
- `trace_portal_route_targets` is the preferred follow-up for compact Widget -> Provider -> route target evidence.
- `download_portal_sources` does not pull linked Script Includes or Angular Providers unless explicitly requested.
- Large portal scans are capped server-side and return warnings when the request exceeds safe defaults.
Pattern matching modes:
| Mode | Behavior |
|------|----------|
| `auto` (default) | Plain strings treated literally, regex-looking patterns remain regex |
| `literal` | Always escape the pattern first; safest for route/token strings |
| `regex` | Use only when you intentionally need regex operators |
---
## Performance Optimizations
The server includes several layers of performance optimization to minimize latency and token usage.
### Serialization
- **orjson backend**: All JSON serialization uses `json_fast` (orjson when available, stdlib fallback). 2-4x faster than stdlib `json` for both loads and dumps.
- **Compact output**: Tool responses are serialized without indentation or extra whitespace, saving 20-30% tokens per response.
- **Double-parse avoidance**: `serialize_tool_output` detects already-compact JSON strings and skips re-serialization.
### Caching
- **OrderedDict LRU cache**: Query results are cached with O(1) eviction using `OrderedDict.popitem()`. 256 max entries, 30-second TTL (600s for stable metadata: schema/scope/choice tables), thread-safe.
- **Tool schema cache**: Pydantic `model_json_schema()` output is cached per model type, avoiding repeated schema generation.
- **Lazy tool discovery**: Only tool modules required by the active `MCP_TOOL_PACKAGE` are imported at startup. Unused modules are skipped entirely.
### Network
- **Browser-grade TLS by default**: The HTTP layer routes through `curl_cffi` with a Chrome impersonation profile (`chrome120` by default), so the TLS handshake is byte-for-byte like a real browser โ instances behind Cloudflare/Akamai or JA3 bot-detection that reject stock Python `requests` work with no extra config. Opt out with `SERVICENOW_TLS_IMPERSONATE=off`.
- **Session keep-alive (browser auth)**: While you're actively working, a background thread pings the instance every 5 minutes with the same lightweight probe the restore path uses, so ServiceNow's sliding idle timeout never kills the session between tool calls โ no more surprise MFA windows after a lunch break. It never opens a browser (a dead session just waits for the next real call to re-auth), stops after 6 hours without real activity, and is tunable via `SERVICENOW_SESSION_KEEPALIVE=off`, `SERVICENOW_SESSION_KEEPALIVE_INTERVAL_S` (default 300, min 60), and `SERVICENOW_SESSION_KEEPALIVE_MAX_IDLE_S` (default 21600).
- **HTTP session pooling**: Persistent session with TCP keep-alive and gzip/deflate compression (60-80% payload reduction on large JSON). The stock-`requests` opt-out path mounts a 20-connection `HTTPAdapter`.
- **Parallel pagination**: `sn_query_all` fetches the first page sequentially for total count, then retrieves remaining pages concurrently via `ThreadPoolExecutor` (up to 4 workers).
- **Persistent debug-browser connection**: The shared debug window is driven over one long-lived worker thread that owns the Playwright driver and a per-endpoint CDP connection, instead of spawning a driver subprocess and a fresh websocket on every tool call. Measured against a real Chromium: ~2ms per warm call vs ~750ms under the old per-call model. A cached connection is revalidated against the window's own DevTools endpoint before every reuse โ a killed window reconnects instead of answering from a stale handle.
- **Dynamic page sizing**: When remaining records fit in a single page (<=100), the page size is enlarged to avoid extra round-trips.
- **Batch API**: `sn_batch` combines multiple REST sub-requests into a single `/api/now/batch` POST, with automatic chunking at the 150-request limit.
- **Parallel chunked M2M queries**: Widget-to-provider M2M lookups split into 100-ID chunks are executed concurrently rather than sequentially.
### Schema & Startup
- **Shallow-copy schema injection**: Confirmation schema (`confirm='approve'`) is injected via lightweight dict copy instead of `copy.deepcopy`, reducing `list_tools` overhead.
- **No-count optimization**: Subsequent pagination pages use `sysparm_no_count=true` to skip server-side total count computation.
- **Payload safety**: Heavy tables (`sp_widget`, `sys_script`, etc.) have automatic field clamping and limit restrictions to prevent context window overflow.
## Local Source Audit
Download and analyze your entire ServiceNow application locally โ no repeated API calls, no context waste.
```
Step 1: download_app_sources(scope="x_company_app") โ All server-side code + cross-scope deps to disk
Step 2: audit_local_sources(source_root="temp/...") โ Analysis + HTML report
```
Step 1 runs `auto_resolve_deps=True` by default: after the in-scope download it scans every
`.js/.html/.xml` file and fetches any referenced `sys_script_include`, `sp_widget`,
`sp_angular_provider`, or `sys_ui_macro` records not already in the bundle โ no matter
what scope they live in. Pulled deps are saved into the same tree with
`"is_dependency": true` in their `_metadata.json`, so the audit in Step 2 sees the
complete call graph. Set `auto_resolve_deps=False` if you only want in-scope records.
> **Tip โ pull a whole scope, including `global`:** pass `scope="global"` to dump every
> global-scope record, or keep your app scope and let `auto_resolve_deps` reach into
> `global` for the records you actually reference. Either way the local bundle is
> self-contained, so analysis runs entirely offline against disk.
### Incremental Sync
Re-downloading a large app on every run is slow and risks timeouts. Pass `incremental=True`
to fetch **only what changed since the last download** โ like `git pull` instead of a fresh
`clone`. Works on both `download_app_sources` and `download_portal_sources`.
```
download_app_sources(scope="x_company_app") # 1st run: full download
download_app_sources(scope="x_company_app", incremental=True) # later: changed records only
```
- **How it works:** the first download records each record's `sys_updated_on` into
`_sync_meta.json`. On an incremental run, every source family queries
`sys_updated_on >= <latest seen>` (server-side timestamps, no clock skew), re-downloads
just those records, and leaves unchanged local files untouched.
- **Deletions:** timestamp deltas can't see deleted records. Add `reconcile_deletions=True`
to list records present locally but gone on the instance โ reported as warnings under
`deletion_candidates`, **never deleted automatically**.
- **First run / no prior data:** falls back to a full download automatically.
- Run a full (non-incremental) download periodically to stay fully in sync.
### Download Safety & Completeness
The download is the source of truth for offline analysis, so it is built to be deterministic and to never *look* complete when it isn't:
- **Scope auto-resolution.** Pass the app **namespace** (`x_company_app`), its **display name** ("My App"), or a `sys_scope` sys_id โ all resolve to the canonical namespace, so the local folder (`temp/<instance>/<namespace>/`) and every query are identical every run. The resolved value is echoed as `scope_resolution`.
- **No silent caps.** If a source family hits `max_records_per_type`, it is flagged loudly: a per-family `capped: true` in `source_types`, the family in `incomplete_types`, and a top-level `complete: false`. A truncated download can never masquerade as a full one.
- **Cross-instance / stale guards.** Pushing back (`update_remote_from_local`) checks the local tree's recorded origin against the connected instance; a resume re-download that keeps a stale local copy preserves the real sync watermark and warns instead of hiding the drift.
- **Relationship metadata at download time.** WidgetโAngular-Provider edges (`_graph.json`) and widgetโCSS/JS-dependency edges (`_dependency_graph.json`) are captured from the live M2M tables during the portal download โ analysis reads the real graph instead of guessing from code.
- **Transitive dependency depth.** Cross-scope deps resolve `2` passes deep by default (conservative). Raise with `SERVICENOW_DEP_MAX_DEPTH` (clamped to `1โ6`) to chase longer AโBโCโD chains.
- **One-call graph build.** Pass `build_graph=True` to `download_app_sources` to run the offline relationship audit right after the download โ no extra API cost.
- **Create โ local sync nudge.** When you create a widget/page on the instance *and* a local tree exists for that scope, the create response adds a `local_out_of_sync` message with the exact `download_portal_sources(...)` command to pull the new record into local. It never writes local files for you.
### What Gets Generated
| File | Purpose |
|------|---------|
| `_audit_report.html` | Self-contained dark-theme HTML report โ open in browser |
| `_cross_references.json` | Who calls who โ Script Include chains, GlideRecord table refs |
| `_graph.json` | Authoritative widgetโAngular Provider edges from the live M2M (not text-guessed) |
| `_dependency_graph.json` | Authoritative widgetโCSS/JS dependency edges from `m2m_sp_widget_dependency` |
| `_page_graph.json` | Pageโwidget placements derived locally from `sp_instance` (no API call) |
| `_orphans.json` | Dead code candidates โ unreferenced SIs, unused widgets |
| `_execution_order.json` | Per-table BR/CS/ACL execution sequence with order numbers |
| `_domain_knowledge.md` | Auto-generated app profile โ table maps, hub scripts, warnings |
| `_schema/*.json` | Field definitions for every referenced table |
| `_sync_meta.json` | Per-family `sys_updated_on` watermark powering incremental sync |
### Individual Download Tools
Use the orchestrator for a full dump, or `download_server_sources` for a targeted single-family refresh:
| Tool | Sources |
|------|---------|
| `download_app_sources` | Full app dump (all families + portal + schema + cross-scope deps) |
| `download_portal_sources` | Widgets, Angular Providers, linked Script Includes |
| `download_server_sources` (`families=`) | Targeted refresh โ `script_includes`, `server_scripts` (BR/Client/Catalog Client), `ui` (Actions/Scripts/Pages/Macros), `api` (Scripted REST/Processors), `security` (ACLs, script-only by default), `admin` (Fix Scripts/Scheduled Jobs/Script Actions/Notifications/Transforms) |
| `download_table_schema` | sys_dictionary field definitions |
All downloads write full source to disk with zero truncation. Only a summary is returned to the LLM context.
---
## Skills
Tools are raw API calls. Skills are what make your LLM actually useful โ verified pipelines with safety gates, rollback, and context-aware sub-agent delegation. **MCP server + skills is the complete setup** for LLM-driven ServiceNow automation.
4 skills today, more coming with every release.
| | Tools Only | Tools + Skills |
|---|---|---|
| Safety | LLM decides | Gates enforced (diff โ preview โ confirm โ apply) |
| Tokens | Source dumps in context | Delegate to sub-agent, summary only |
| Accuracy | LLM guesses tool order | Verified pipeline |
| Rollback | Might forget | Server-side version history (ServiceNow Versions tab / update sets) |
### Install Skills
```bash
# Claude Code
uvx --from mfa-servicenow-mcp servicenow-mcp-skills claude
# OpenAI Codex
uvx --from mfa-servicenow-mcp servicenow-mcp-skills codex
# OpenCode
uvx --from mfa-servicenow-mcp servicenow-mcp-skills opencode
# Antigravity
uvx --from mfa-servicenow-mcp servicenow-mcp-skills antigravity
```
The installer downloads the skill files from this repository's `skills/` directory and places them in a project-local LLM directory. No authentication or configuration needed.
> If `servicenow-mcp-skills` is blocked by security policy on Windows, call it as a module instead โ same behavior:
>
> ```bash
> python -m servicenow_mcp.setup_skills claude
> ```
| Client | Install Path | Auto-Discovery |
|--------|-------------|----------------|
| Claude Code | `.claude/commands/servicenow/` | `/servicenow` slash commands appear on next startup |
| OpenAI Codex | `.codex/skills/servicenow/` | Skills loaded on next agent session |
| OpenCode | `.opencode/skills/servicenow/` | Skills loaded on next session |
| Antigravity | `.gemini/antigravity/skills/servicenow/` | Skills activated on next session |
**How it works:** Each skill is a standalone Markdown file with YAML frontmatter (metadata) and pipeline instructions. The LLM client reads these files from the install path and exposes them as callable commands or skill triggers.
**Update:** Re-run the same install command โ it replaces all existing skill files (clean install, no merge).
**Remove skills only:** delete the skill install directory manually (for example `rm -rf .claude/commands/servicenow/`).
### Skill Categories
| Category | Skills | Purpose |
|----------|--------|---------|
| `analyze/` | 1 | **local source audit** โ cross-references, dead code, execution order, HTML report |
| `explore/` | 1 | **flow trigger tracing** โ which workflows/flows fire when a table changes |
| `manage/` | 2 | **app source download**, **local sync** (diff โ push with conflict detection) |
### Skill Metadata
Each skill includes metadata that helps LLMs optimize execution:
```yaml
context_cost: low|medium|high # โ high = delegate to sub-agent
safety_level: none|confirm|staged # โ staged = mandatory diff/preview/apply
delegatable: true|false # โ can run in sub-agent to save context
triggers: ["์์ ฏ ๋ถ์", "analyze widget"] # โ LLM trigger matching
```
For the full skill reference, see [skills/SKILL.md](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/skills/SKILL.md).
### MCP Resources (Built-in Skill Guides)
Skills are also exposed as **MCP resources** directly from the server โ no client-side installation required. Any MCP-compliant client can discover and read them on demand.
```
# List available skill guides
list_resources โ skill://manage/local-sync, skill://manage/app-source-download, ...
# Read a specific guide
read_resource("skill://manage/local-sync") โ full pipeline with safety gates
```
Tools that have a matching skill guide show a `โ skill://...` hint in their description. The guide content is **pull-based** โ zero token cost until the client actually reads it.
| Feature | Client-side Skills | MCP Resources |
|---------|-------------------|---------------|
| Availability | Requires install command | Built-in, any client |
| Token cost | Loaded by client | Pull on demand (0 until read) |
| Discovery | Slash commands / triggers | `list_resources` |
| Best for | Power users, slash commands | Universal guidance |
## Docker
API Key auth only (MFA browser auth requires GUI, not available in containers).
```bash
docker run -it --rm \
-e SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com \
-e SERVICENOW_AUTH_TYPE=api_key \
-e SERVICENOW_API_KEY=your-api-key \
ghcr.io/jshsakura/mfa-servicenow-mcp:latest
```
See [Client Setup Guide](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/CLIENT_SETUP.md#docker-api-key-only) for local build options.
## Developer Setup
If you want to modify the source locally:
```bash
git clone https://github.com/jshsakura/mfa-servicenow-mcp.git
cd mfa-servicenow-mcp
uv venv
uv pip install -e ".[browser,dev]"
uvx --with playwright playwright install chromium
```
### Running Tests
```bash
uv run pytest
```
### Linting & Formatting
```bash
uv run black src/ tests/
uv run isort src/ tests/
uv run ruff check src/ tests/
uv run mypy src/
```
### Building
```bash
uv build
```
> Windows: see [Windows Installation Guide](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/WINDOWS_INSTALL.md)
---
## Documentation
- [LLM Setup Guide](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/llm-setup.md) โ AI-guided one-line installation flow
- [Client Setup Guide](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/CLIENT_SETUP.md) โ Installer-first setup plus fallback client configs
- [Tool Inventory](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/TOOL_INVENTORY.md) โ Complete tool list by category and package
- [Windows Installation Guide](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/WINDOWS_INSTALL.md)
- [Catalog Guide](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/catalog.md) โ Service catalog CRUD and optimization
- [Change Management](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/change_management.md) โ Change request lifecycle and approval
- [Workflow Management](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/docs/workflow_management.md) โ Workflow (wf_workflow engine) and Flow Designer tools
- [Korean README](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/README.ko.md)
---
## Related Projects and Acknowledgements
- This repository includes tools consolidated and refactored from earlier internal / legacy ServiceNow MCP implementations. The current surface is organized around bundled `manage_*` tools (see [tool_utils.py](https://github.com/jshsakura/mfa-servicenow-mcp/blob/main/src/servicenow_mcp/utils/tool_utils.py)).
- This project is focused on safe, diff-first MCP server use cases: every write goes through confirm + write-guards (concurrent-edit, duplicate-create, publish, Flow Designer), and source edits are diffed against the live remote before they are pushed.
---
## License
Apache License 2.0
TDQS
Scored across 32 tools
Several clusters overlap: get_widget_bundle, get_portal_component_code, and get_metadata_source all fetch source/body; search_server_code vs search_portal_regex_matches vs query_local_graph all search; download_portal_sources vs download_app_sources both download. Descriptions work hard to draw boundaries ('last resort', 'LEGACY ONLY', 'portal only'), which mitigates but does not eliminate misselection risk.
Most tools follow a verb_noun pattern (get_page, download_attachment, verify_deployment_xml), but the sn_-prefixed namespace tools (sn_health, sn_query, sn_schema, sn_discover) mix a noun-prefix convention with the rest. Readable overall, but conventions are mixed rather than uniform.
At 32 tools this is heavy and well past the 25-tool threshold, with several near-duplicate source-fetching and search tools that could be consolidated. The domain is complex enough to justify a large surface, but the set is over-scoped from a coherence standpoint.
Coverage is strong: discovery, CRUD for Scripted REST/script includes/widget dependencies, flow and workflow editing, portal/widget lifecycle, deployment XML export/verify, and offline auditing. Minor gaps exist (e.g. no explicit update-set or user/role management), but core ServiceNow dev workflows are well covered.