servicenow-mcp
The most comprehensive ServiceNow MCP server. 19 tools for full CRUD, CMDB graph traversal, background scripts, ATF testing, and more.
Documentation
@onlyflows/servicenow-mcp
The most comprehensive ServiceNow MCP server. 19 tools for full CRUD, append-only incident journals, CMDB graph traversal, ATF testing, multi-instance profiles, and more.
Built by OnlyFlows · Published by @onlyflowstech
Installation
npm install -g @onlyflows/servicenow-mcp
servicenow-mcp-setupRun bare on a terminal, `servicenow-mcp-setup` walks you through the whole
thing in one command: it generates the local auth material, asks for your
instance and credential, verifies that credential against the instance,
asks which tables to grant, writes the profile, and registers any supported
client whose CLI is installed. The credential is entered at a hidden prompt —
never an argument, never shell history — and nothing is written to the profile
until the instance has accepted it, so a typo or a wrong password leaves
nothing behind.
ServiceNow MCP setup
This walks through one ServiceNow connection end to end. Nothing is
written to the profile until your credential is verified against the
instance. Press Ctrl+C at any point to stop; nothing will be saved.
Profile name [dev]: dev
ServiceNow instance (for example dev12345.service-now.com): dev12345.service-now.com
Authentication:
1) basic (default)
2) oauth
3) apikey
Choice [1]: 1
How should the credential be stored?:
1) encrypted (default)
2) reference
Choice [1]: 1
ServiceNow username: integration.user
Enter each secret now. Input is hidden — nothing you type from here
is displayed.
credential:
Verifying against the instance...
ok https://dev12345.service-now.com accepted the credential.
Table access is deny-by-default: a profile with no rules denies every
tool call. Grant the narrowest set that does the job; you can add more
later with servicenow-mcp-setup grant.
Tables to allow for READS:
1) Just incident (default)
2) Common ITSM set (incident,change_request,problem,task,sys_user)
3) All tables (*)
4) Enter a custom list
5) None
Choice [1]: 1
Tables to allow for WRITES:
1) Just incident
2) Common ITSM set (incident,change_request,problem,task,sys_user)
3) All tables (*)
4) Enter a custom list
5) None (default)
Choice [5]: 1
Wrote profile dev.
Register this server with codex and claude-code? [Y/n]: y
ServiceNow MCP doctor
ok node: v22.11.0
ok server command: servicenow-mcp resolves on PATH; clients spawn it over stdio
ok config directory: ~/.servicenow-mcp (0700)
ok profile dev: instance: https://dev12345.service-now.com
ok profile dev: credential: basic credential resolves from its encrypted source
ok profile dev: table access: 1 read, 1 write, 1 target(s)
All checks passed.
Setup complete.
Profile dev
Reads incident
Writes incident
Transport stdio (each client spawns its own servicenow-mcp)
Clients codex, claude-code
Start codex or claude-code and it will launch the server itself.
There is nothing to keep running between sessions.There is nothing to start. The server speaks stdio: each registered client
spawns its own copy of `servicenow-mcp` on demand and talks to it over that
process's stdin and stdout. Setup finishes by running the same checks `doctor`
runs, so you can re-run them any time:
servicenow-mcp-setup doctor --profile dev`npx @onlyflows/servicenow-mcp@latest setup` runs the same wizard without a
global install, and `servicenow-mcp setup` is an alias for
`servicenow-mcp-setup`.
Scripted setup
Pass any flag, or run without a terminal, and the wizard steps aside for the
original non-interactive behavior — so CI and provisioning scripts are
unaffected. `--non-interactive` forces it explicitly:
servicenow-mcp-setup --non-interactive --clients none --json
servicenow-mcp-profile create --name dev --instance https://yourinstance.service-now.com \
--auth-type oauth --client-id --source reference --provider env
servicenow-mcp-setup grant --profile dev --read incident,problem --write incidentThe `servicenow-mcp-setup` commands
| Command | Purpose |
|---|---|
| `servicenow-mcp-setup` | Generate local auth material and register supported clients |
| `servicenow-mcp-setup client --client ` | Print copy-pasteable config for a client |
| `servicenow-mcp-setup grant --profile --read ` | Add table access rules to a profile |
| `servicenow-mcp-setup doctor` | Diagnose the install and print remedies |
`--force` regenerates the owner/client identifiers but deliberately preserves
`SN_PROFILE_ENCRYPTION_KEY`, which decrypts every credential envelope in
`config.json`. Add `--help` to any command for its full options.
> The server runs as you, and the client list is the boundary. Nothing
> listens on a port, so nothing off this machine can reach it and no web page
> can drive it. What that leaves is the registration: every MCP client
> registered here can spawn the server and use whatever ServiceNow access your
> profiles grant, as your integration account. Keep the grants narrow, and
> remove a client you no longer use with `claude mcp remove servicenow-mcp` or
> `codex mcp remove servicenow-mcp`. No ServiceNow credential is copied into a
> client's configuration file — the spawned server reads the profile and its
> encryption key from the owner-only `~/.servicenow-mcp` directory.
Connecting a client
`servicenow-mcp-setup` registers Codex and Claude Code automatically when their
CLI is on `PATH`. For everything else:
servicenow-mcp-setup client --client claude-desktop
servicenow-mcp-setup client --client allEvery supported client speaks stdio natively, so each one is configured the
same way: a command to spawn.
claude mcp add servicenow-mcp -- servicenow-mcp
codex mcp add servicenow-mcp -- servicenow-mcpstdio is the default transport for both CLIs, so there is no transport flag to
pass. For clients configured by file:
{
"mcpServers": {
"servicenow-mcp": { "command": "servicenow-mcp", "args": [] }
}
}| Client | Configured by | Config location |
|---|---|---|
| Claude Code | `claude mcp add` or `.mcp.json` | project or `--scope user` |
| Codex | `codex mcp add` or `config.toml` | `~/.codex/config.toml` |
| Cursor | file | `~/.cursor/mcp.json` or `.cursor/mcp.json` |
| VS Code | file (`"type": "stdio"`) | `.vscode/mcp.json` or user `mcp.json` |
| Claude Desktop | file | `claude_desktop_config.json` |
| Windsurf | file | `~/.codeium/windsurf/mcp_config.json` |
No client holds a ServiceNow credential. The spawned server resolves its own
from `~/.servicenow-mcp`, which is owner-only.
Full per-client blocks are in
> `servicenow-mcp` must be on the spawning client's `PATH`. A global npm
> install puts it there. If it is not — a project-local install, or a GUI
> client with a different `PATH` — register the absolute entrypoint instead;
> `servicenow-mcp-setup doctor` checks this and prints the exact command.
Guided profile creation from your client
After the bootstrap, a connected MCP client can walk you through profile
creation:
Add a ServiceNow MCP profile for https://yourinstance.service-now.comThe MCP prompt is `servicenow-mcp.add-profile`. It guides profile naming, auth
mode, and least-privilege default-deny access, and deliberately does not
ask you to paste passwords, API keys, bearer tokens, OAuth client secrets, or
encryption keys into chat.
What gets installed
- `servicenow-mcp` — the MCP server itself, spawned by a client over stdio
- `servicenow-mcp-profile` — manages profile credentials out of band
- `servicenow-mcp-setup` — bootstrap, client config, grants, and diagnosis
Manual configuration without the bootstrap
The server keeps configuration explicit. A manual run needs:
- audit identity: `MCP_OWNER_ID` and `MCP_CLIENT_ID` (optional; they label
audit records and default to `local-owner`/`local-client`)
- one named ServiceNow profile, either in `~/.servicenow-mcp/config.json` or
through `SN_PROFILE_NAME` + `SN_INSTANCE` + auth-specific `SN_*` variables
- per-profile table access rules; unconfigured access denies everything
The `SN_*` environment path builds a profile **only when
`~/.servicenow-mcp/config.json` does not exist**. Once you create a profile
file, `SN_ALLOWED_READ_TABLES`, `SN_ALLOWED_WRITE_TABLES`, and
`SN_TABLE_ACCESS_TARGETS` stop applying and the rules must live on the profile
(`servicenow-mcp-setup grant`). This is the most common cause of a server that
denies every call; `servicenow-mcp-setup doctor` detects it.
The server reads `~/.servicenow-mcp/server.env` itself at startup, because the
client that spawns it supplies its own environment and will not have sourced
anything. A value already present in the environment always wins over that
file, so container and CI configuration is unaffected.
For a quick environment-only run with no profile file — driving the server by
hand over a pipe, or from a client that passes environment variables — inject
protected values from your keychain or secret manager and pass only non-secret
values on the command line:
MCP_OWNER_ID=local-owner \
MCP_CLIENT_ID=local-client \
SN_PROFILE_NAME=dev \
SN_INSTANCE=https://yourinstance.service-now.com \
SN_USER=your_user \
SN_ALLOWED_READ_TABLES=incident,problem,change_request \
SN_ALLOWED_WRITE_TABLES=incident,change_request \
SN_TABLE_ACCESS_TARGETS='[{"table":"incident","kind":"canonical","tools":["sn_query","sn_get","sn_create","sn_update","sn_incident_add_comment","sn_incident_add_work_note","sn_delete","sn_batch"],"closureComplete":true,"relatedTables":["incident"]},{"table":"problem","kind":"canonical","tools":["sn_query","sn_get"],"closureComplete":true,"relatedTables":["problem"]},{"table":"change_request","kind":"canonical","tools":["sn_query","sn_get"],"closureComplete":true,"relatedTables":["change_request"]}]' \
servicenow-mcpDo not put `SN_PASSWORD`, OAuth client secrets, or API keys in command history.
Inject them into the server's environment from your approved secret mechanism,
or leave them in the owner-only profile file where the server can resolve them
without any client seeing them.
Source install for development
Use source install only for development or unreleased changes:
git clone https://github.com/onlyflowstech/servicenow-mcp.git
cd servicenow-mcp
npm install
npm run build
npm startFor local development with a rebuild and source maps:
npm run devContainer deployment applies to the dormant HTTP transport rather than a 2.0 stdio install; see Production container deployment and the enterprise boundary.
Multi-Instance Profiles
Manage multiple ServiceNow instances (dev, test, prod, PDI) with named profiles. Every tool call must select one configured profile explicitly; V2 has no active-profile or default-profile fallback.
Setup
Create profiles with the CLI rather than by hand — it captures credentials
without putting them in argv, and it writes the file with the right ownership
and mode:
servicenow-mcp-profile create --name dev --instance https://mydev.service-now.com --auth-type basic --username admin --source reference --provider env
servicenow-mcp-profile create --name prod --instance https://myprod.service-now.com --auth-type basic --username api.user --source reference --provider env
servicenow-mcp-setup grant --profile dev --read incident,problem --write incident
servicenow-mcp-setup grant --profile prod --read incidentThe resulting `~/.servicenow-mcp/config.json` looks like this. Note
`tableAccess`: a profile without it denies every tool call.
{
"version": 2,
"profiles": {
"dev": {
"instance": "https://mydev.service-now.com",
"username": "admin",
"credential": { "type": "secret_ref", "provider": "env", "reference": "SN_PASSWORD_DEV" },
"description": "Development instance",
"tableAccess": {
"readTables": ["incident", "problem"],
"writeTables": ["incident"],
"targets": [
{
"table": "incident",
"kind": "canonical",
"tools": ["sn_query", "sn_get", "sn_aggregate", "sn_schema", "sn_create", "sn_update"],
"closureComplete": true,
"relatedTables": ["incident"]
},
{
"table": "problem",
"kind": "canonical",
"tools": ["sn_query", "sn_get", "sn_aggregate", "sn_schema"],
"closureComplete": true,
"relatedTables": ["problem"]
}
]
}
}
}
}Have the approved supervisor, orchestrator, keychain, or secret manager provide
the values referenced by `SN_PASSWORD_DEV` and `SN_PASSWORD_PROD` in the
environment the server is spawned with, or in `~/.servicenow-mcp/server.env`,
which the server falls back to. Do not type either value into a shell command,
command argument, dotenv file, or command history.
Each `targets` entry asserts `closureComplete: true`, meaning `relatedTables`
lists every backing, ancestor, and descendant table the operation can reach.
When a table extends another, declare it:
`servicenow-mcp-setup grant --profile dev --read change_request --related change_request=task`.
Related tables join the allowlist but get no target of their own, so a caller
cannot address them directly.
Credential Options
| Format | Example | Description |
|---|---|---|
| Legacy environment reference | `"env:SN_PASSWORD_DEV"` | Read-compatible V1 form; migrated to a structured reference on the next write |
| Secret reference | `{"type":"secret_ref","provider":"env","reference":"SN_PASSWORD_DEV"}` | Provider-neutral reference resolved only for the selected request |
| Encrypted envelope | `{"type":"encrypted","version":1,...}` | AES-256-GCM value created by the protected administration CLI |
Plaintext profile secrets are rejected. Use the installed `servicenow-mcp-profile`
operator command to create, inspect, rotate, or remove profiles; it reads secrets
through a protected prompt or bounded standard input and rejects credential-bearing
command-line arguments. Encrypted sources require exactly 32 random bytes encoded
as base64 or base64url in `SN_PROFILE_ENCRYPTION_KEY`, supplied separately by the
deployment secret mechanism and never stored in the profile file. See the
profile credential and administration guide.
The legacy `env:VAR_NAME` indirection remains readable for every secret field:
`credential`, `clientSecret`, and `apiKey`.
Using Profiles
Pass the `profile` parameter on every tool call:
- *"query incidents on prod"* — calls `sn_query` with `profile: "prod"`
- *"get incident INC0010001 on dev"* — calls `sn_get` with `profile: "dev"`
- *"show me the dev profile endpoint"* — calls the read-only `sn_profile` diagnostic with `profile: "dev"`
Profiles are created and changed out of band by the service operator. Legacy
`default_profile` metadata is ignored and is not persisted on the next
administrative write; the MCP boundary never consults it.
`SN_*` configuration compatibility
If no config file exists, canonical `SN_*` connection values are exposed only
through an explicit `SN_PROFILE_NAME` mapping. For example, set
`SN_PROFILE_NAME=dev` with `SN_INSTANCE`, `SN_USER`, and `SN_PASSWORD`, then
call tools with `profile: "dev"`. Without `SN_PROFILE_NAME`, bare connection
variables create no profile and cannot route a request. Secret values remain
runtime-only environment references and are never persisted as plaintext.
Authentication
Three ServiceNow auth types are available per profile, selected with `authType` (default: `basic`). Profile configuration is managed out of band by the service operator. This is the only authentication involved: the transport is stdio, so there is no endpoint in front of the server to protect.
> Heads up: ServiceNow's inbound Basic Auth restriction program is phasing out basic auth for API requests — instances can start hard-rejecting it at any time (exemptions: Web-Service-Access-Only accounts or the `snc_basic_auth_api_access` role). OAuth is the recommended auth type. The server prints a startup warning for basic-auth profiles.
OAuth 2.0 (recommended)
`client_credentials` grant (default) — create an OAuth API endpoint client in ServiceNow (System OAuth → Application Registry) and reference the secret via `env:` indirection:
{
"version": 2,
"profiles": {
"dev": {
"instance": "https://mydev.service-now.com",
"authType": "oauth",
"clientId": "your-oauth-client-id",
"clientSecret": "env:SN_CLIENT_SECRET",
"description": "OAuth client_credentials"
}
}
}`password` grant — set `grantType` and provide the user credentials as well:
{
"instance": "https://mydev.service-now.com",
"authType": "oauth",
"grantType": "password",
"clientId": "your-oauth-client-id",
"clientSecret": "env:SN_CLIENT_SECRET",
"username": "integration.user",
"credential": "env:SN_PASSWORD_DEV"
}Tokens are cached until shortly before their `expires_in` expiry and refreshed automatically (including a single refresh + retry on 401). Token responses are never logged.
API key
For instances using Inbound Authentication Profiles with API keys. The header name is configurable (default `x-sn-apikey`):
{
"instance": "https://mydev.service-now.com",
"authType": "apikey",
"apiKey": "env:SN_API_KEY",
"apiKeyHeader": "x-sn-apikey"
}Basic (default, deprecated by ServiceNow)
{
"instance": "https://mydev.service-now.com",
"username": "admin",
"credential": "env:SN_PASSWORD_DEV"
}On a 401, the error explains the Basic Auth restriction program (KB3096078) and how to move to OAuth.
Timeouts & retries
Every request is bounded by a timeout (default 30s; per-profile `timeoutMs` or env `SN_TIMEOUT_MS`) and retried up to twice with exponential backoff on 429/502/503/504, honoring `Retry-After`. POST requests are only retried on 429 — never after a 5xx that may have executed side effects.
ServiceNow upstream can return HTTP 429 with an empty body, while this server's own HTTP rate-limit rejections return a short JSON body plus `Retry-After`. Treat the status and `Retry-After` header as authoritative; do not benchmark or validate tool success from response latency or body shape alone. In normal ServiceNow instances, plan around the Background throttling default of roughly 120 requests per 60 seconds per identity unless the instance limit is explicitly raised.
Why This MCP Server?
Most ServiceNow MCP integrations are read-only and support a handful of
tables. This service offers a broader tool catalog behind explicit,
deny-by-default table and tool policy:
| Feature | Others | @onlyflows/servicenow-mcp |
|---|---|---|
| Query records | ✅ | ✅ |
| Create records | ❌ | ✅ |
| Update records | ❌ | ✅ |
| Delete records | ❌ | ✅ (with safety confirm) |
| Bulk operations | ❌ | ✅ (dry-run by default) |
| Aggregations (COUNT/AVG/MIN/MAX/SUM) | ❌ | ✅ |
| Table schema introspection | ❌ | ✅ |
| CMDB relationship traversal | ❌ | ✅ (recursive, configurable depth) |
| Instance health monitoring | ❌ | ✅ (version, nodes, jobs, stats) |
| Attachment management | ❌ | ✅ (list, upload, download; inline base64, no host filesystem) |
| System log queries | ❌ | ✅ |
| Code search across artifacts | ❌ | ✅ |
| Table/app/plugin discovery | ❌ | ✅ |
| ATF test execution | ❌ | 🚧 listing/results available; execution currently policy-denied |
| Natural language interface | ❌ | 🚧 currently policy-denied pending a typed access plan |
| Background scripts | ❌ | 🚧 on the roadmap (SNS-39) |
| Multi-instance profiles | ❌ | ✅ (named profiles, per-call override) |
| Total tools | 1–3 | 19 |
Quick Start
The server speaks stdio and requires Node.js 20 or newer. The shortest path is
the bootstrap described under Installation:
servicenow-mcp-setupThat registers your clients; each one spawns the server when it needs it.
The rest of this section is the manual environment path, for a deployment that
injects everything from a supervisor or secret manager, or for driving the
server by hand over a pipe.
Use `.env.example` only as a non-secret configuration
inventory. Its protected-value assignments are intentionally empty; inject
ServiceNow secrets through a supervisor, orchestrator, keychain, or secret
manager rather than filling a repository dotenv file.
> The `SN_ALLOWED_*` and `SN_PROFILE_NAME` variables below build a profile only
> when `~/.servicenow-mcp/config.json` does not exist. With a profile file
> present, put the rules on the profile with `servicenow-mcp-setup grant`.
export MCP_OWNER_ID="your-owner-id"
export MCP_CLIENT_ID="your-client-id"
export SN_ALLOWED_READ_TABLES="incident,problem,change_request"
export SN_ALLOWED_WRITE_TABLES="incident,change_request"
export SN_TABLE_ACCESS_TARGETS='[{"table":"incident","kind":"canonical","tools":["sn_query","sn_get","sn_create","sn_update","sn_incident_add_comment","sn_incident_add_work_note","sn_delete","sn_batch"],"closureComplete":true,"relatedTables":["incident"]},{"table":"problem","kind":"canonical","tools":["sn_query","sn_get"],"closureComplete":true,"relatedTables":["problem"]},{"table":"change_request","kind":"canonical","tools":["sn_query","sn_get"],"closureComplete":true,"relatedTables":["change_request"]}]'
export SN_PROFILE_NAME="dev"
export SN_INSTANCE="https://yourinstance.service-now.com"
export SN_USER="your_username"
npm run build
npm startBefore running those non-secret commands, the approved runtime secret
mechanism must already have injected the authentication-specific ServiceNow
secret into the process. Do not enter it in this shell block.
A client connects by spawning the command; there is no URL and no header:
{
"mcpServers": {
"servicenow-mcp": { "command": "servicenow-mcp", "args": [] }
}
}JSON-RPC messages travel on the child's stdin and stdout, one newline-delimited
message per frame. Everything else the server reports — startup warnings, and
one structured JSON-line event per completed tool call — goes to stderr,
which your client captures as its server log. There is no per-message size
limit imposed by the transport.
For complete official SDK and independent client examples, safe two-profile
setup, and crossed profile/result/audit checks, see
Breaking changes in 2.0
Every tool call must name a `profile`, and table access is deny-by-default.
Those two require action on every install. The transport is stdio, as it was in
`1.0.0`, so a client configured with a `command` entry keeps working.
The breaking changes from `1.0.0`, each with a before/after example, are in
Deployment guides for the dormant HTTP transport
2.0 ships stdio only. The container image, the health and shutdown contract,
the operations runbook, and the Secure MCP Tunnel adapter all describe the
which no CLI path reaches. They are kept because that transport is a planned
enterprise deployment shape, not because they apply to a 2.0 install — nothing
in this section is needed to use this server.
artifact, non-root runtime, provenance and scanning
- Remote operations runbook — health
interpretation, telemetry, alerts, rotation, incident response
outbound-only Secure MCP Tunnel adapter
Tools Reference
Core CRUD
| Tool | Description |
|---|---|
| `sn_query` | Query an approved table with structured filters, field selection, pagination, and sorting; bounded raw reads require an explicit policy rule |
| `sn_get` | Get a single record by sys_id from an approved table |
| `sn_create` | Create an incident using only controlled ordinary writable fields |
| `sn_update` | Update one incident selected by exact `sys_id`, using only controlled ordinary writable fields; rejects `comments` and `work_notes` with dedicated-tool migration guidance |
| `sn_incident_add_comment` | Append one bounded customer-visible comment to an incident; repeated calls append again |
| `sn_incident_add_work_note` | Append one bounded internal work note to an incident; repeated calls append again |
| `sn_delete` | Delete a record on an approved write table (requires `confirm: true`) |
| `sn_batch` | Bulk update/delete using a required structured filter and dry-run safety (raw encoded selectors are prohibited; requires `confirm: true` to execute) |
Analytics & Schema
| Tool | Description |
|---|---|
| `sn_aggregate` | COUNT, AVG, MIN, MAX, SUM with grouping |
| `sn_schema` | Table field definitions, types, references |
| `sn_health` | Instance version, cluster nodes, stuck jobs, key stats |
CMDB & Operations
| Tool | Description |
|---|---|
| `sn_relationships` | CMDB CI graph traversal — upstream/downstream/both, configurable depth |
| `sn_attach` | List attachments, return downloads as inline base64, and upload inline base64 content; never touches a host filesystem path |
| `sn_syslog` | Query system logs with severity/source/time filters |
| `sn_codesearch` | Search business rules, script includes, client scripts, etc. |
| `sn_discover` | Discover tables, scoped apps, store apps, plugins |
Testing & Automation
| Tool | Description |
|---|---|
| `sn_atf` | List ATF tests/suites and get results; `run`/`run-suite` currently fail closed |
| `sn_nl` | Currently fails closed until natural-language composition emits a complete typed access plan |
`sn_script` (background script execution) shipped in 1.0.0 as an unimplemented
stub and is not published in 2.0 — it does not appear in `tools/list`. Use
`sn_query` and `sn_batch` instead. See Roadmap.
Profile Management
| Tool | Description |
|---|---|
| `sn_profile` | Inspect non-secret metadata for one explicitly named profile; profile configuration is operator-managed out of band |
Environment Variables
MCP runtime
| Variable | Required | Default | Description |
|---|---|---|---|
| `MCP_OWNER_ID` | ❌ | `local-owner` | Stable non-secret owner identifier recorded in request/tool context and every audit record. A label, not a credential; it is never checked. |
| `MCP_CLIENT_ID` | ❌ | `local-client` | Stable non-secret client identifier recorded in request/tool context and every audit record. A label, not a credential; it is never checked. |
That is the whole list. There is no bind address, port, `Host`/`Origin`
allowlist, concurrency cap, connection cap, shutdown grace period, bearer
token, or rate limit, because there is no listener: a client spawns the server
and owns its lifetime.
The server reads these from its environment and falls back to
`~/.servicenow-mcp/server.env` for any it does not find there. That file is
owner-only (mode `0600`) and is where `servicenow-mcp-setup` writes the
generated identifiers and `SN_PROFILE_ENCRYPTION_KEY`. It exists because the
client that spawns the server supplies its own environment and will not have
sourced anything. A value already present in the environment always wins, so a
container or CI runner that passes configuration directly is unaffected.
`servicenow-mcp-setup doctor` reports that file's mode and contents.
Observability
Each completed tool call emits one JSON-line event to stderr, which the
spawning client captures as its server log:
{"schemaVersion":1,"type":"mcp_tool","observedAtMs":1737000000000,"latencyMs":42,
"correlationId":"stdio--","ownerIdHash":"sha256:...",
"clientIdHash":"sha256:...","tool":"sn_query","profile":"dev",
"instance":"https://dev00001.service-now.com","outcome":"success","reason":null,
"errorCategory":null,"retry":null,"retryAfterSeconds":null}Tool audits classify request cancellation and request-deadline expiration
separately from handler failures. Configured owner/client identifiers are
represented only by SHA-256 pseudonyms; headers, URLs/query strings, bodies,
credentials, tokens, config objects, and exception text are not event fields.
Telemetry never blocks a tool result: stderr backpressure retains at most 256
pending lines, drops excess events, and emits a
`{"type":"telemetry_dropped","count":N}` summary after the stream drains.
A correlation ID is `stdio--`. The session half is fixed
for the life of one spawned server, so every record from one client session
groups together; the invocation half is fresh per call. Neither half is derived
from anything the caller sent.
stdout carries the protocol and nothing else. Every diagnostic goes to
stderr. One stray byte on stdout would desynchronize the client's JSON-RPC
parser and end the session, so there is no such thing as a harmless
`console.log` on the startup or request path; `test/stdio-entrypoint.test.ts`
spawns a real server and asserts it.
Arguments that fail a tool's schema are rejected by the MCP SDK before any
handler runs, so they return an error to the caller but produce **no audit
event** — the execution context that would issue one is never opened. Every
call that reaches a tool is audited.
Body size and concurrency (dormant HTTP runtime only)
None of this applies to the shipped server. It describes the dormant HTTP
runtime, which no CLI path reaches — see
the enterprise release boundary
— and binds only someone embedding this package and calling `createHttpRuntime`
directly. Over stdio there is no request-body limit and no admission ceiling.
Request body size and concurrency trade directly against each other under the
512 MiB admission ceiling:
| `maxBodyBytes` | Highest concurrency that starts | Effect |
|---|---|---|
| 1 MiB (shipped default) | 2 | 416 MiB budgeted; the intended configuration |
| 1.75 MiB | 2 | 512 MiB — exactly at the ceiling |
| 2 MiB | 1 | Single-flight: every tool call serializes |
| 5.75 MiB | 1 | the last value that starts at all |
| above 5.75 MiB | none | startup fails at any concurrency |
Two failure modes are worth knowing:
- At 2 MiB and above the server is single-flight. Concurrency 2 no longer
fits, so the runtime can only start at 1 and every tool call queues behind
every other one. There is no warning and no log line — it presents as "the
server got slow", and one large upload blocks every other tool for its
duration. Nothing connects the cause to the effect.
- Above 5.75 MiB the server refuses to start. A constructor `throw`, not a
clamp. The message names `maxConcurrentRequests` and `maxBodyBytes` but gives
neither the ceiling, the arithmetic, nor a working value; the table above is
the way forward.
`maxBodyBytes` is not operator-configurable. `src/http-entrypoint.ts`
builds the request policy with only `allowedHosts` and `allowedOrigins`, so the
limit is always 1 MiB and no environment variable changes it.
Metadata cache
Stable ServiceNow metadata reads are cached per instance and credential identity
for 24 hours by default. The default cached table patterns are
`sys_glide_object`, `sys_dictionary`, `sys_db_object`, `sys_app`, `sys_plugins`,
`sys_metadata*`, and `sys_flow*`. Set `metadataCache.ttlMs` and
`metadataCache.tables` on a file-backed profile, or `SN_METADATA_CACHE_TTL_MS`
/ `SN_METADATA_CACHE_TABLES` for the explicit environment profile. `sn_query`,
`sn_get`, and `sn_schema` accept `force_recache: true` to refresh metadata. A
cache hit performs a lightweight `sys_updated_on` probe and is used only when no
matching metadata rows changed since the previous sync.
Prerequisite: a resolvable session timezone. The freshness probe compares
`sys_updated_on`, which ServiceNow evaluates in the *session user's* timezone,
not UTC. Resolving that zone requires the authenticating account's `user_name`
to look up `sys_user.time_zone`, falling back to the `glide.sys.default.tz`
property. When it cannot be resolved the cache disables itself for that
identity rather than assuming UTC — a wrong assumption would silently serve
stale metadata for the length of the offset. The server logs a warning naming
the reads it needs.
This has one consequence worth knowing before you configure it:
| Profile auth | Metadata caching |
|---|---|
| Basic | works — the profile carries a username |
| OAuth password grant | works — the profile carries a username |
| OAuth client_credentials | disabled — no username to resolve a timezone from |
| API key | disabled — no username to resolve a timezone from |
The profile also needs read access to `sys_user` (for `time_zone`) and
`sys_properties` (for `glide.sys.default.tz`) for the lookup to succeed at all.
The honest tradeoff: the cache only ever saved payload bytes, never round
trips — a cache hit still costs one freshness probe. So on a client-credentials
or API-key profile the practical loss is bandwidth on `sys_dictionary` reads,
not latency. If you are choosing an auth mode, this should not be the deciding
factor.
Deletions become visible within one TTL. The freshness probe detects
updates, not deletes, so a row deleted upstream stays served until its entry
expires and is re-fetched — up to 24 hours at the default TTL. This is accepted
behavior, not a defect; lower `SN_METADATA_CACHE_TTL_MS` or pass
`force_recache: true` if you need a deletion reflected sooner.
Unrestricted field selection (`fields=all`, `response_format=detailed`)
Both resolve to a wildcard selection. They previously dropped `sysparm_fields`
entirely, so ServiceNow returned every column and a wide table could breach the
1 MiB cumulative upstream cap and fail the call outright. They are now
bounded to at most 100 columns.
The cap bounds the *upstream request*, not the response — truncating after
receipt would not help, because the bytes have already crossed the wire and
already breached the limit. Columns are resolved from `sys_dictionary`, walking
`super_class` so an extended table contributes its inherited fields. Ordering is
deterministic: the table's curated default projection first in its declared
order, then every remaining column alphabetically. Defaults lead so a capped
result stays useful; alphabetical afterwards because dictionary row order is not
stable across instances. Every failure path falls back to the table's bounded
default projection, never to dropping `sysparm_fields`.
`sn_query` reports truncation through its `hint` field. `sn_get` will carry the
same notice shortly.
Two behavior changes worth stating plainly, because they change what a caller
gets back:
- Journal fields are now returned. `comments` and `work_notes` are included
in the resolved set for `fields=all` and `detailed`. See
Journal content and field selection.
- Sensitive-looking field names are excluded from the resolved set entirely.
A name matching the sensitive-field pattern is never *requested*, rather than
being requested and scrubbed on arrival. Under the old wildcard behavior the
value crossed the wire and was then removed; naming it in `sysparm_fields`
would have pulled it over deliberately, which is worse.
Journal content and field selection
Journal content on `incident` — `comments` (customer-visible) and `work_notes`
(internal) — is readable through `fields=all`, `response_format: "detailed"`, an
explicit `fields=comments`, and `sn_schema`. **The default projection still
excludes it**, so an ordinary `sn_query` or `sn_get` does not return it.
Stated as a fact rather than a warning: journal content on real instances
routinely contains customer PII, so asking for all fields on `incident` returns
customer-visible commentary along with everything else. Operators granting
`incident` reads to an agent should know that. If that is not wanted, grant a
narrower read via the field policy rather than relying on the default
projection, since the caller chooses `fields`.
Attachment payloads
`sn_attach` never reads from or writes to host filesystem paths. Uploads use a
safe leaf `file_name` plus `content_base64`; downloads return
`content_base64`, `file_name`, `content_type`, and `size_bytes`. Download also
requires the owning `table` and record `sys_id`, which are policy-authorized
and verified against attachment metadata before any bytes are returned.
Size limit: 10 MiB decoded, and that is the only one. The stdio transport
frames messages by newline with no size bound, so the tool's own limits are
what apply — 10 MiB of decoded bytes for an upload, and a separate 10 MiB raw
budget for a download. The tool's `content_base64` schema description carries
the authoritative figure.
The ~760 KiB practical ceiling documented before 2.1 came from the HTTP body
limit, which no longer applies: the client and server share a pipe, not an
envelope. Base64 still inflates a file by about a third in the message itself,
so a 10 MiB attachment is roughly a 13.3 MiB JSON frame — large, but the
transport will carry it.
Treat `sn_attach` as suitable for logs, configs, screenshots, and documents
rather than bulk transfer; a very large frame still costs memory in both the
client and the server, and ServiceNow's own UI or a dedicated integration is a
better path for bulk file movement.
Table access policy
2.0 denies every ServiceNow table by default. Table access is selected per
profile, not from a process-wide implicit default. File-backed profiles define
`tableAccess` in `~/.servicenow-mcp/config.json`; the explicit `SN_PROFILE_NAME`
environment profile maps the `SN_ALLOWED_*` variables into that one profile only
when no profile file exists. If a profile has no table rules, it denies all.
Write the rules with the CLI rather than by hand — it validates the result with
the same loader the server uses at request time:
servicenow-mcp-setup grant --profile dev --read incident,problem --write incident
servicenow-mcp-setup grant --profile dev --read cmdb_ci --tools sn_query,sn_relationships
servicenow-mcp-setup grant --profile dev --read change_request --related change_request=taskAllowlist entries may be exact table names or the literal `*` to allow every
table for that operation. Table names are trimmed, lowercased, deduplicated,
and must be valid ServiceNow identifiers.
> `tableAccess` is the sole table-level authority. There is no longer a
> built-in list of tables the server refuses unconditionally. A profile that
> grants a table gets it, subject only to ServiceNow's own per-user ACLs — so a
> grant of `sys_script`, `sys_user_role`, or a credential table is honored.
> Writing `sys_script` is server-side script execution under the integration
> account. Least privilege now lives entirely in the grant and in the roles you
> give that account; grant the narrowest set that does the job, and prefer an
> account whose ServiceNow roles cannot reach what the grant does not need.
| Variable | Required | Default | Description |
|---|---|---|---|
| `SN_ALLOWED_READ_TABLES` | ❌ | deny all | Tables permitted for read operations on the explicit `SN_PROFILE_NAME` environment profile only. Use `*` to allow every readable table. |
| `SN_ALLOWED_WRITE_TABLES` | ❌ | deny all | Tables permitted for create, update, incident journal append, delete, upload, and confirmed batch operations on the explicit `SN_PROFILE_NAME` environment profile only. Use `*` to allow every writable table. Write permission never implies read permission. |
| `SN_TABLE_ACCESS_TARGETS` | Required for exact allowlisted caller-addressable tables; optional with `*` | `[]` | Trusted JSON classification with `table`, exact permitted `tools`, `kind` (`canonical`, `alias`, `view`, or `extension`), literal `closureComplete: true`, and the complete backing/ancestor/descendant `relatedTables` closure. With `*`, omitted target entries use the wildcard operation grant; explicit target entries can still narrow tools and validate related-table closure. |
| `SN_FIELD_POLICY_DEFINITIONS` | Required for custom/generic table fields | built-in finite policy | Trusted JSON object keyed by table name or `*`. Each entry may define `defaults`, `readable`, and `writable`; `readable`/`writable` accept exact field arrays or `"*"`. Sensitive field names are still denied. Use `{"*":{"defaults":["sys_id"],"readable":"*","writable":[]}}` for broad read-only custom-table exploration. Use `writable:"*"` only for intentional broad mutation access. |
| `SN_ENCODED_QUERY_READ_POLICY` | ❌ | deny all | Trusted JSON object containing bounded `rules` for an exact `sn_query`/table pair. Each rule requires `maxLength`, `maxTerms`, readable `fields`, supported `operators`, `maxLimit`, `maxOffset`, and `maxResponseBytes`. No rule can authorize a write or another tool. |
Invalid policy configuration fails startup. A composed tool is admitted only
when its complete backing-table plan is allowed before the first ServiceNow
client access. Every related target must have the same read or write
permission, so a base table, alias, view, or extension cannot bypass an
unlisted backing or descendant table.
Related permission does not make a dependency directly caller-addressable
without its own target entry. Field policy narrows what a granted table exposes; it does not deny a table the
operator granted. Sensitive-looking field names remain excluded regardless. Build the
complete target catalog from approved ServiceNow metadata and treat it as
trusted startup configuration; omit a target when reachability cannot be proven
complete. File-backed profile example:
{
"version": 2,
"profiles": {
"dev": {
"instance": "https://dev.service-now.com",
"username": "integration.user",
"credential": "env:SN_PASSWORD",
"tableAccess": {
"readTables": ["incident", "sys_dictionary"],
"writeTables": ["incident"],
"targets": [
{
"table": "incident",
"kind": "canonical",
"tools": ["sn_query", "sn_get", "sn_create", "sn_update"],
"closureComplete": true,
"relatedTables": ["incident"]
}
]
}
}
}
}`sn_nl` and ATF `run`/`run-suite` currently fail closed because they do not emit
a complete typed side-effect plan; use the corresponding typed tool instead.
Incident journal fields are append-only: generic `sn_update` rejects `comments`
and `work_notes` before credentials or client creation. Use
`sn_incident_add_comment` or `sn_incident_add_work_note` with exactly
`profile`, incident `sys_id`, and bounded `content`; grant the selected tool
explicitly on the `incident` target as well as write access to that table.
ServiceNow profiles
> Note: A profile file is authoritative when present. Without one,
> `SN_PROFILE_NAME` must explicitly map the canonical connection variables to
> a named profile; there is no synthetic or default profile.
| Variable | Required | Default | Description |
|---|---|---|---|
| `SN_PROFILE_NAME` | ✅* | — | Explicit name for the process-local environment profile; required before bare connection variables define any profile. |
| `SN_INSTANCE` | ✅* | — | Instance URL (e.g. `https://yourinstance.service-now.com`) |
| `SN_USER` | ✅* | — | ServiceNow username (basic auth / OAuth password grant) |
| `SN_PASSWORD` | ✅* | — | ServiceNow password (basic auth / OAuth password grant) |
| `SN_AUTH_TYPE` | ❌ | `basic` | Auth scheme: `basic`, `oauth`, or `apikey` |
| `SN_CLIENT_ID` | ❌ | — | OAuth client id (`SN_AUTH_TYPE=oauth`) |
| `SN_CLIENT_SECRET` | ❌ | — | OAuth client secret (`SN_AUTH_TYPE=oauth`) |
| `SN_GRANT_TYPE` | ❌ | `client_credentials` | OAuth grant: `client_credentials` or `password` |
| `SN_API_KEY` | ❌ | — | API key (`SN_AUTH_TYPE=apikey`) |
| `SN_API_KEY_HEADER` | ❌ | `x-sn-apikey` | Header the API key is sent in |
| `SN_TIMEOUT_MS` | ❌ | `30000` | Per-request timeout in milliseconds |
| `SN_DISPLAY_VALUE` | ❌ | `true` | Default display value mode (`true`, `false`, `all`) |
| `SN_REL_DEPTH` | ❌ | `3` | Default CMDB relationship traversal depth |
*`SN_PROFILE_NAME` and `SN_INSTANCE` are not required when using
`~/.servicenow-mcp/config.json`. Authentication-specific variables depend on
the selected auth type.
Usage Examples
Once connected, your AI assistant can:
Query incidents:
> "On dev, show me all P1 incidents assigned to the Network team" (`profile: "dev"`)
Create a record:
> "On staging, create an incident for the approved VPN test" (`profile: "staging"`)
Aggregate data:
> "On prod, how many incidents are grouped by priority?" (`profile: "prod"`)
Check health:
> "Run the dev instance health check" (`profile: "dev"`)
CMDB traversal:
> "On prod, show upstream dependencies for email-server-01" (`profile: "prod"`)
Schema introspection:
> "On dev, what fields are on change_request?" (`profile: "dev"`)
Code search:
> "On dev, find business rules that reference GlideRecord('incident')" (`profile: "dev"`)
ATF testing:
> "On staging, list the approved ATF suite" (`profile: "staging"`)
Explicit profile selection:
> "Query incidents on dev" (the client sends `profile: "dev"` on that call)
The client must translate every example into a tool invocation containing that
exact `profile`; no prior call creates default, active, or switch-profile state.
Safety Features
This server is designed for production use with multiple safety layers:
- Delete operations require explicit `confirm: true`
- Batch operations run in dry-run mode by default — shows match count without making changes
- Bulk operations require `confirm: true` to leave dry-run mode
- Unclassified composition is denied — `sn_nl` and ATF execution do not run until they can emit complete typed access plans
- Table access is deny-by-default with independent exact read/write allowlists and non-configurable sensitive-table denials
- User input is neutralized before interpolation into encoded queries (`^` is stripped from filter values — ServiceNow's query syntax has no escape sequence)
Development
Have the approved local supervisor or keychain inject the selected ServiceNow
authentication secret before starting the process. The commands below contain
non-secret configuration only; never prepend or append protected values on the
command line.
# Clone
git clone https://github.com/onlyflowstech/servicenow-mcp.git
cd servicenow-mcp
# Install & build
npm install
npm run build
# Run the server directly, speaking stdio on this terminal's pipes
MCP_OWNER_ID=local-owner \
MCP_CLIENT_ID=local-client \
SN_ALLOWED_READ_TABLES=incident,problem,change_request \
SN_ALLOWED_WRITE_TABLES=incident,change_request \
SN_TABLE_ACCESS_TARGETS='[{"table":"incident","kind":"canonical","tools":["sn_query","sn_get","sn_create","sn_update","sn_incident_add_comment","sn_incident_add_work_note","sn_delete","sn_batch"],"closureComplete":true,"relatedTables":["incident"]},{"table":"problem","kind":"canonical","tools":["sn_query","sn_get"],"closureComplete":true,"relatedTables":["problem"]},{"table":"change_request","kind":"canonical","tools":["sn_query","sn_get"],"closureComplete":true,"relatedTables":["change_request"]}]' \
SN_PROFILE_NAME=dev \
SN_INSTANCE=https://yourinstance.service-now.com \
SN_USER=your_user \
npm start
# Build and run with source maps for local development
MCP_OWNER_ID=local-owner \
MCP_CLIENT_ID=local-client \
SN_ALLOWED_READ_TABLES=incident,problem,change_request \
SN_ALLOWED_WRITE_TABLES=incident,change_request \
SN_TABLE_ACCESS_TARGETS='[{"table":"incident","kind":"canonical","tools":["sn_query","sn_get","sn_create","sn_update","sn_incident_add_comment","sn_incident_add_work_note","sn_delete","sn_batch"],"closureComplete":true,"relatedTables":["incident"]},{"table":"problem","kind":"canonical","tools":["sn_query","sn_get"],"closureComplete":true,"relatedTables":["problem"]},{"table":"change_request","kind":"canonical","tools":["sn_query","sn_get"],"closureComplete":true,"relatedTables":["change_request"]}]' \
SN_PROFILE_NAME=dev \
SN_INSTANCE=https://yourinstance.service-now.com \
SN_USER=your_user \
npm run devTesting with MCP Inspector
Use the isolated, exactly locked Inspector toolchain on Node.js 22.19 or newer;
the server itself remains supported on Node.js 20. Set the non-secret
`MCP_PROFILE`, install from `tools/inspector/package-lock.json`, and run the
local-only launcher:
MCP_PROFILE=dev npm run inspector(Run `npm ci --prefix tools/inspector --engine-strict --ignore-scripts` first.)
The launcher prints the exact STDIO command and arguments to enter in the
Inspector UI. There is no endpoint and no bearer to type. Include the explicit
profile in every call. The Inspector spawns the server itself and inherits a
scrubbed environment with every `MCP_*` and `SN_*` value removed; the server
still resolves its credential, because it reads the owner-only
`~/.servicenow-mcp/server.env` at startup rather than relying on what it was
handed. The launcher does not expose Inspector beyond loopback or create a
tunnel.
`npm run smoke` is the second client. It spawns `dist/index.js` the same way and
needs only `MCP_PROFILE`:
MCP_PROFILE=dev npm run smokeSee `docs/RELEASE-VALIDATION.md` for the release
gates, write confirmation, evidence, and rollback procedure.
Roadmap
- [x] stdio transport — the client spawns the server; a Streamable HTTP runtime exists but is dormant
- [x] OAuth 2.0 authentication support (client_credentials + password grants, API keys)
- [ ] sn_script background-script execution (SNS-39) — future state, deliberately not shipped in 2.0; requires automating the `sys.scripts.do` UI endpoint with session auth
- [ ] Streaming for large result sets
- [ ] Caching for schema and relationship lookups
License
MIT © OnlyFlows
Built with ❤️ by ·
Frequently asked questions
What is servicenow-mcp?
servicenow-mcp is The most comprehensive ServiceNow MCP server. 19 tools for full CRUD, CMDB graph traversal, background scripts, ATF testing, and more.
How do I install servicenow-mcp?
Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.
Is servicenow-mcp open source?
Yes — it is hosted on GitHub at https://github.com/onlyflowstech/servicenow-mcp and has 12 stars.
Related MCP tools
The go-to web for your AI coding agent — local-first search, fetch, crawl & research over MCP. No API keys, no cloud, $0/query. Public beta.
MCP server that enables AI assistants to interact with Google Gemini CLI, leveraging Gemini's massive token window for large file analysis and codebase understanding
Universal AI context generator. Saves thousands of tokens per conversation in Claude Code, Cursor, Copilot, Codex, and more.
A desktop MCP client designed as a tool unitary utility integration, accelerating AI adoption through the Model Context Protocol (MCP) and enabling cross-vendor LLM API orchestration.
Code research platform for AI agents; find, understand, and prove context across your code and all of GitHub, in a fraction of the tokens. One toolset, MCP or CLI
Unity MCP acts as a bridge between AI assistants and your Unity Editor. Give your LLM tools to manage assets, control scenes, edit scripts, and automate tasks within Unity.
Run your own MCP server? See who uses it and what to fix.
Measure it with TrackMCP