Metadata-Version: 2.4
Name: mfa-servicenow-mcp
Version: 1.22.2
Summary: A Model Context Protocol (MCP) server implementation for ServiceNow
Author: jshsakura
License:                                  Apache License
                                   Version 2.0, January 2004
                                http://www.apache.org/licenses/
        
           TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
        
           1. Definitions.
        
              "License" shall mean the terms and conditions for use, reproduction,
              and distribution as defined by Sections 1 through 9 of this document.
        
              "Licensor" shall mean the copyright owner or entity authorized by
              the copyright owner that is granting the License.
        
              "Legal Entity" shall mean the union of the acting entity and all
              other entities that control, are controlled by, or are under common
              control with that entity. For the purposes of this definition,
              "control" means (i) the power, direct or indirect, to cause the
              direction or management of such entity, whether by contract or
              otherwise, or (ii) ownership of fifty percent (50%) or more of the
              outstanding shares, or (iii) beneficial ownership of such entity.
        
              "You" (or "Your") shall mean an individual or Legal Entity
              exercising permissions granted by this License.
        
              "Source" form shall mean the preferred form for making modifications,
              including but not limited to software source code, documentation
              source, and configuration files.
        
              "Object" form shall mean any form resulting from mechanical
              transformation or translation of a Source form, including but
              not limited to compiled object code, generated documentation,
              and conversions to other media types.
        
              "Work" shall mean the work of authorship, whether in Source or
              Object form, made available under the License, as indicated by a
              copyright notice that is included in or attached to the work.
        
              "Derivative Works" shall mean any work, whether in Source or Object
              form, that is based on (or derived from) the Work and for which the
              editorial revisions, annotations, elaborations, or other modifications
              represent, as a whole, an original work of authorship.
        
              "Contribution" shall mean any work of authorship, including
              the original version of the Work and any modifications or additions
              to that Work or Derivative Works thereof, that is intentionally
              submitted to the Licensor for inclusion in the Work by the copyright
              owner or by an individual or Legal Entity authorized to submit on
              behalf of the copyright owner.
        
              "Contributor" shall mean Licensor and any individual or Legal Entity
              on behalf of whom a Contribution has been received by the Licensor and
              subsequently incorporated within the Work.
        
           2. Grant of Copyright License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              copyright license to reproduce, prepare Derivative Works of,
              publicly display, publicly perform, sublicense, and distribute the
              Work and such Derivative Works in Source or Object form.
        
           3. Grant of Patent License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              patent license to make, have made, use, offer to sell, sell, import,
              and otherwise transfer the Work.
        
           4. Redistribution. You may reproduce and distribute copies of the
              Work or Derivative Works thereof in any medium, with or without
              modifications, and in Source or Object form, provided that You
              meet the following conditions:
        
              (a) You must give any other recipients of the Work or
                  Derivative Works a copy of this License; and
        
              (b) You must cause any modified files to carry prominent notices
                  stating that You changed the files; and
        
              (c) You must retain, in the Source form of any Derivative Works
                  that You distribute, all copyright, patent, trademark, and
                  attribution notices from the Source form of the Work.
        
           5. Submission of Contributions. Unless You explicitly state otherwise,
              any Contribution intentionally submitted for inclusion in the Work
              by You to the Licensor shall be under the terms and conditions of
              this License, without any additional terms or conditions.
        
           6. Trademarks. This License does not grant permission to use the trade
              names, trademarks, service marks, or product names of the Licensor.
        
           7. Disclaimer of Warranty. Unless required by applicable law or
              agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
              WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
              implied, including, without limitation, any warranties or conditions
              of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
              PARTICULAR PURPOSE.
        
           8. Limitation of Liability. In no event and under no legal theory,
              shall any Contributor be liable to You for damages, including any
              direct, indirect, special, incidental, or consequential damages of
              any character arising as a result of this License or out of the use
              or inability to use the Work.
        
           9. Accepting Warranty or Additional Liability. While redistributing
              the Work or Derivative Works thereof, You may choose to offer,
              and charge a fee for, acceptance of support, warranty, indemnity,
              or other liability obligations and/or rights consistent with this
              License.
        
           END OF TERMS AND CONDITIONS
        
           Copyright 2025 jshsakura
        
           Licensed under the Apache License, Version 2.0 (the "License");
           you may not use this file except in compliance with the License.
           You may obtain a copy of the License at
        
               http://www.apache.org/licenses/LICENSE-2.0
        
           Unless required by applicable law or agreed to in writing, software
           distributed under the License is distributed on an "AS IS" BASIS,
           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
           See the License for the specific language governing permissions and
           limitations under the License.
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Requires-Python: >=3.11
Requires-Dist: curl-cffi<1,>=0.7.0
Requires-Dist: httpx<1,>=0.28.0
Requires-Dist: mcp[cli]<2,>=1.28.1
Requires-Dist: orjson<4,>=3.9.0
Requires-Dist: pydantic<3,>=2.0.0
Requires-Dist: python-dotenv<2,>=1.0.0
Requires-Dist: pyyaml<7,>=6.0
Requires-Dist: requests<3,>=2.32.4
Provides-Extra: browser
Requires-Dist: playwright>=1.44.0; extra == 'browser'
Provides-Extra: dev
Requires-Dist: black>=25.1.0; extra == 'dev'
Requires-Dist: isort==5.13.2; extra == 'dev'
Requires-Dist: mypy>=1.0.0; extra == 'dev'
Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.5.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.0.1; extra == 'dev'
Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
Requires-Dist: types-requests>=2.32.0; extra == 'dev'
Description-Content-Type: text/markdown

# 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.

[![PyPI version](https://img.shields.io/pypi/v/mfa-servicenow-mcp.svg)](https://pypi.org/project/mfa-servicenow-mcp/)
[![Python Version](https://img.shields.io/pypi/pyversions/mfa-servicenow-mcp)](https://pypi.org/project/mfa-servicenow-mcp/)
[![CI](https://github.com/jshsakura/mfa-servicenow-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/jshsakura/mfa-servicenow-mcp/actions/workflows/ci.yml)
[![Docker](https://img.shields.io/badge/ghcr.io-mfa--servicenow--mcp-blue?logo=docker)](https://ghcr.io/jshsakura/mfa-servicenow-mcp)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![GitHub Pages](https://img.shields.io/badge/GitHub%20Pages-live-blue?logo=github)](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
- **66 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`) — fetch a record's attachment file(s) (xlsx, PDF, Word, …) to local disk by attachment sys_id or by parent `table`+`record`; resolves a record's attachments automatically and writes bytes to disk so the LLM reads them from `saved_path`
- **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

### 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` | 29 | ~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` | 31 | ~8.2K | ⚠️ standard + incident and change operational writes |
| `portal_developer` | 41 | ~10.6K | ⚠️ standard + portal, changeset, script include, and local-sync delivery writes |
| `platform_developer` | 41 | ~10.8K | ⚠️ standard + workflow, Flow Designer, UI policy, incident/change, and script writes |
| `full` | 55 | ~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).
- **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
