← Back to cipheryogi portfolio

FastMCP Server for ServiceNow Instances

One Python script wraps the ServiceNow Table API as 60 MCP tools. Register the same command once per instance, change only the environment variables, and an AI client can read and write platform tables, Flow Designer logs, generative AI logs and the whole AI Agent Studio object model across every instance you work on, with nothing hosted and nothing listening on a network port.

60 tools over stdio One script, N instances Python 3.13 · uv · FastMCP Basic Auth or OAuth Nothing listens on a port Native console compared

Companion guides: Claude Code File Structure & Configuration (where instructions, settings, MCP configuration and credentials live) and Claude Code Capabilities (tools, agents, skills, hooks, permission modes, models and automation).

1. What This Setup Is

One Python script, launched once per ServiceNow instance, exposing 60 tools to an MCP client.

The problem it solves. You want an AI client, Claude Code or any other MCP-capable client, to read and write ServiceNow data across several instances without building a separate integration per instance and without hosting anything. The answer here is deliberately small.

One scriptserver_single.py is a FastMCP server that wraps the ServiceNow Table API as 60 tools. You add a tool by editing this one file.
One process per instanceThe same command is registered once per instance. Only the environment variables differ, so the script reads which instance to talk to at start-up. There is no per-instance branch in the code.
Two launchersClient A is Claude Code, running the script from a project folder. Client B is a second desktop MCP client with its own local copy. Both run server_single.py over stdio.
Many instances, one patternEach instance gets its own server entry with the same command, all over Basic Auth. Personal dev instances can use an admin account; customer instances should use a dedicated service account. Examples are in section 5.

Facts at a glance

ItemValue
Tools exposed60
Launchers2: Claude Code, and a second desktop MCP client
Instances2 in this example; the pattern scales to any number
Transportstdio
AuthBasic Auth, with the OAuth password grant optional
StackPython 3.13 · uv · FastMCP
Alternative coveredServiceNow's native MCP Server Console, in sections 14 and 15

Placeholders used in this guide

PlaceholderWhat to substitute
<instance-a>, <instance-b>, <customer-a>Example ServiceNow instances you are wiring up: two personal dev instances and one customer instance.
https://<instance>.service-now.comAny instance base URL.
<username>, <password>The integration account's credentials.
<client-id>, <client-secret>OAuth Application Registry credentials.
<project-root>The folder holding the server script.
~/Your home folder.
<client-b-folder>The folder holding Client B's copy of the script.
SN_The environment-variable prefix.
sn_The tool-name prefix.
About the prefixes. SN_ and sn_ are labels you choose when you register the server. The variable names are identical for every instance, and each process reads its own instance URL from SN_INSTANCE, so the prefix carries no meaning beyond readability. Two reading notes: every filesystem path below is shown relative to <project-root> or ~/, and inside code blocks placeholders are printed in grey.

2. Architecture

Two launchers, two copies of the script, any number of instances, all over stdio and HTTPS.

flowchart TD
  subgraph LOCAL["Your machine"]
    direction TB
    CC["MCP client A
Claude Code · CLI or editor extension"] CW["MCP client B
second desktop MCP client"] CJ["Client A config
env block per server"] CI["Client B config
env placeholders per server"] UVA["uv run python server_single.py
one process per server"] UVB["uv run server_single.py
one process per server"] SA["Script A
<project-root>/mcp-server/server_single.py"] SB["Script B · local copy
~/<client-b-folder>/server_single.py"] CC --> CJ CJ --> UVA UVA --> SA CW --> CI CI --> UVB UVB --> SB end SA -- "HTTPS REST
/api/now/table/..." --> E1["Client A: one server per instance
e.g. <instance-a>, <instance-b>, <customer-a>"] SB -- "HTTPS REST" --> E2["Client B: one server per instance
same pattern"] classDef client fill:#e8f1ff,stroke:#3975c6,color:#132238; classDef cfg fill:#fff5dd,stroke:#9a6500,color:#5a3d00; classDef code fill:#f1eaff,stroke:#7b52b9,color:#2f1f4b; classDef inst fill:#e8f7f1,stroke:#157a5a,color:#103e31; class CC,CW client; class CJ,CI cfg; class UVA,UVB,SA,SB code; class E1,E2 inst;
MCP client applications Client configuration, which holds the credentials Processes and script copies ServiceNow instances
Key idea: the AI client never talks to ServiceNow directly. It sends a tool call to the local Python process over stdin and stdout, the process makes the HTTPS call, and the result comes back as text. Nothing listens on a network port on your machine.

Per-instance registration fan-out

flowchart TD
  SCRIPT["server_single.py
one file, 60 tool functions"] REG["Registered once per instance
same command, different env vars"] SCRIPT --> REG REG --> P1["Process 1
SN_INSTANCE=https://<instance-a>.service-now.com"] REG --> P2["Process 2
SN_INSTANCE=https://<instance-b>.service-now.com"] REG --> PN["Process N
SN_INSTANCE=https://<instance-n>.service-now.com"] P1 --> T1["Tools exposed as
mcp__servicenow-sn-<instance-a>__sn_*"] P2 --> T2["Tools exposed as
mcp__servicenow-sn-<instance-b>__sn_*"] PN --> TN["Tools exposed as
mcp__servicenow-sn-<instance-n>__sn_*"] classDef code fill:#f1eaff,stroke:#7b52b9,color:#2f1f4b; classDef proc fill:#e8f1ff,stroke:#3975c6,color:#132238; classDef tool fill:#e8f7f1,stroke:#157a5a,color:#103e31; class SCRIPT,REG code; class P1,P2,PN proc; class T1,T2,TN tool;

3. Inventory of Folders and Files

Every location that matters, and whether it is shared between servers or local to one client.

flowchart TD
  H["~ · home"] --> CJ["Client A config file
live credentials, env block per server"] H --> VE["~/.venvs/mcp-server
shared Python environment"] H --> AM["~/<client-b-folder>
Client B copy: server_single.py, pyproject.toml, uv.lock, .venv"] H --> LIB["~/Library/Application Support/<client-b>
integrations.json"] H --> OD["<project-root>"] OD --> MS["mcp-server
server_single.py, pyproject.toml, uv.lock, .env · unused"] OD --> DS["design, skills"] classDef a fill:#e8f1ff,stroke:#3975c6,color:#132238; classDef g fill:#e8f7f1,stroke:#157a5a,color:#103e31; classDef y fill:#fff5dd,stroke:#9a6500,color:#5a3d00; class H,OD a; class CJ,LIB,MS y; class VE,AM,DS g;
File or folderPurposeShared or local
Client A user config, which is ~/.claude.json for Claude CodeOne entry per server under mcpServers, including the env block with instance URL, username and password. The live source of credentials for Client A.Local to the machine, shared across all servers
<project-root>/mcp-server/Project folder for Script A. uv is pointed at this folder with --directory.Shared by every Client A server
<project-root>/mcp-server/server_single.pyScript A, the FastMCP server with all 60 tools, about 2,534 lines.Shared by every Client A server
<project-root>/mcp-server/pyproject.tomlDeclares Python >=3.13 and three dependencies: mcp[cli]>=1.27.0, python-dotenv>=1.2.2, requests>=2.33.1.Shared
<project-root>/mcp-server/uv.lockPinned dependency versions.Shared
<project-root>/mcp-server/.python-versionContains 3.13.Shared
<project-root>/mcp-server/.envLegacy credentials file for one instance only. Not used in practice, as section 7 explains.Local, legacy
<project-root>/mcp-server/server_single.backup_<timestamp>.pyBackup of Script A taken before an edit.Local
swap_instance.sh, add_instance.sh, setup.sh and their .ps1 versionsLegacy scripts from an earlier one-instance-at-a-time approach. They rewrite server_single.py with sed. Not needed for the setup in this guide.Local, legacy
Reference copies of Script A and Script BSnapshot copies of both scripts kept next to this guide's source document. Nothing launches them, so editing them changes nothing.Reference only
~/.venvs/mcp-serverShared Python virtual environment on Python 3.13.x. Every Client A server points at it with UV_PROJECT_ENVIRONMENT.Shared by all Client A servers
~/<client-b-folder>/Project folder for Script B, the Client B copy. Contains server_single.py, pyproject.toml, uv.lock, .python-version and its own .venv.Local to Client B
~/<client-b-folder>/server_single.pyScript B, about 2,478 lines. The same 60 tools, different start-up code, as section 13 explains.Local to Client B
~/Library/Application Support/<client-b>/integrations.jsonClient B's list of MCP servers. Each stdio server has a command and an env map whose values are placeholders of the form $MCP_ENV_<server-id>_<VARIABLE>.Local to Client B
/opt/homebrew/bin/uvThe uv package manager and runner, installed with Homebrew on Apple silicon.Shared

4. Prerequisites

Install these once per machine, then never think about them again.

ItemWhat it is, and how to checkNotes
HomebrewInstalls uv. Check with brew --version.Install it from brew.sh. On Apple silicon it lives in /opt/homebrew.
uvRuns the script and creates the virtual environment. Check with /opt/homebrew/bin/uv --version.brew install uv. Verified against uv 0.11.x.
Python 3.13Required by pyproject.toml (>=3.13). Check with python3.13 --version.uv can fetch it for you with uv python install 3.13. The shared environment here uses 3.13.x.
An MCP clientRegisters and launches the servers. For Claude Code, check with claude --version.Verified against Claude Code 2.1.x.
ServiceNow accountA user that can read, and write if you want the authoring tools, the tables you will touch. This example uses an admin account on the personal dev instances and service accounts on customer instances.Basic Auth must be allowed for the account. If the account is forced through SSO or MFA, create a local service account instead.
NetworkThe machine must reach https://<instance>.service-now.com. Check with curl -sI https://<instance>.service-now.com | head -1.VPN and proxy rules apply exactly as they do for a browser.

5. Setup A: Claude Code

The primary runtime. Script in the project folder, credentials in the client config.

flowchart TD
  S0["0 · Prerequisites
Homebrew, uv, Python 3.13, Claude Code"] --> S1["1 · Create the project folder
pyproject.toml + server_single.py"] S1 --> S2["2 · Create the shared venv
uv sync with UV_PROJECT_ENVIRONMENT"] S2 --> S3["3 · Test the script by hand
with the three env variables"] S3 --> Q{"Prints the OK banner
and stays running?"} Q -- "No" --> T["Go to Troubleshooting"] Q -- "Yes" --> S4["4 · Register one server per instance
claude mcp add --scope user"] S4 --> S5["5 · Restart the client
tools appear as mcp__servicenow-sn-NAME__*"] S5 --> S6["6 · Prove it reaches the instance
query sys_user, limit 1"] S6 --> S7["7 · Optional: allow-list the read tools"] classDef a fill:#e8f1ff,stroke:#3975c6,color:#132238; classDef g fill:#e8f7f1,stroke:#157a5a,color:#103e31; classDef y fill:#fff5dd,stroke:#9a6500,color:#5a3d00; class S0,S1,S2,S3,S4,S5 a; class S6,S7 g; class Q,T y;

Step 1 · Create the project folder

Create the folder and put two files in it: server_single.py, whose structure section 8 describes, and pyproject.toml.

mkdir -p "<project-root>/mcp-server"
cd "<project-root>/mcp-server"

The whole of pyproject.toml:

[project]
name = "mcp-server"
version = "0.1.0"
description = "ServiceNow MCP server"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
  "mcp[cli]>=1.27.0",
  "python-dotenv>=1.2.2",
  "requests>=2.33.1",
]
Check: ls shows pyproject.toml and server_single.py. Create an empty README.md as well, because pyproject.toml names it.

Step 2 · Create the shared virtual environment

Point uv at one environment outside the project folder, then install the dependencies into it. All servers share this environment.

export UV_PROJECT_ENVIRONMENT="$HOME/.venvs/mcp-server"
cd "<project-root>/mcp-server"
/opt/homebrew/bin/uv sync

Check the environment is real and the dependencies import:

~/.venvs/mcp-server/bin/python --version                            # prints Python 3.13.x
~/.venvs/mcp-server/bin/python -c "import mcp, requests, dotenv"     # prints nothing

Step 3 · Test the script by hand

Run it with the three environment variables set for one instance. This proves the script and its dependencies work before the AI client is involved.

cd "<project-root>/mcp-server"
SN_INSTANCE="https://<instance-a>.service-now.com" \
SN_USERNAME="<username>" \
SN_PASSWORD="<password>" \
UV_PROJECT_ENVIRONMENT="$HOME/.venvs/mcp-server" \
/opt/homebrew/bin/uv run python server_single.py
Check: the terminal prints [OK] Connected, then a short banner between two lines of = signs, and then waits. Press Ctrl+C to stop it.
Caveat. That output appears even if the credentials are wrong, because the script does not call the instance at start-up. Section 13 covers it.

Step 4 · Register one server per instance

Use the Claude Code CLI at user scope so the servers are available in every project. Repeat for each instance, changing the server name, URL, username and password. The variable names stay the same for every instance, only the values change.

claude mcp add servicenow-sn-<instance-a> --scope user \
  -e SN_INSTANCE="https://<instance-a>.service-now.com" \
  -e SN_USERNAME="<username>" \
  -e SN_PASSWORD="<password>" \
  -e UV_PROJECT_ENVIRONMENT="$HOME/.venvs/mcp-server" \
  -- /opt/homebrew/bin/uv --directory "<project-root>/mcp-server" run python server_single.py

claude mcp add servicenow-sn-<instance-b> --scope user \
  -e SN_INSTANCE="https://<instance-b>.service-now.com" \
  -e SN_USERNAME="<username>" \
  -e SN_PASSWORD="<password>" \
  -e UV_PROJECT_ENVIRONMENT="$HOME/.venvs/mcp-server" \
  -- /opt/homebrew/bin/uv --directory "<project-root>/mcp-server" run python server_single.py

The result is stored in the Claude Code user config. One entry looks like this:

"servicenow-sn-<instance-a>": {
  "type": "stdio",
  "command": "/opt/homebrew/bin/uv",
  "args": [
    "--directory",
    "<project-root>/mcp-server",
    "run", "python", "server_single.py"
  ],
  "env": {
    "SN_INSTANCE": "https://<instance-a>.service-now.com",
    "SN_USERNAME": "<username>",
    "SN_PASSWORD": "<password>",
    "UV_PROJECT_ENVIRONMENT": "<home>/.venvs/mcp-server"
  }
}
Check: claude mcp list shows each server you added as connected.

Example server entries

Every server in ~/.claude.json is built this way. All of them use the same command, arguments and shared venv as step 4. Only the env values differ, as these examples show.

Server nameSN_INSTANCESN_USERNAME
servicenow-sn-<instance-a>https://<instance-a>.service-now.comadmin
servicenow-sn-<instance-b>https://<instance-b>.service-now.comadmin
servicenow-sn-<customer-a>https://<customer-a>.service-now.com<service-account>

Step 5 · Restart the client and confirm the tools

Restart the editor window or the CLI session. Tools are named mcp__<server name>__<tool name>, for example mcp__servicenow-sn-<instance-a>__sn_query.

Check: asking the client to list its ServiceNow tools shows 60 per server.

Step 6 · Prove it reaches the instance

Ask the client: "Using the <instance-a> server, query sys_user with limit 1." A returned record proves the whole chain: client, stdio, the script, HTTPS, Basic Auth, the instance.

Check: the reply contains a record with a sys_id. An HTTP 401 means the username or password is wrong.

Step 7 · Optional: avoid permission prompts for read tools

Add entries such as mcp__servicenow-sn-<instance-a>__sn_query to the permissions.allow list in ~/.claude/settings.json. Leave write and delete tools off the list so the client still asks first.

6. Setup B: A Second Desktop MCP Client

The second runtime. A separate copy of the script, launched by a second desktop client application.

Client B is any second desktop MCP client that can launch a stdio server. The transferable part is the shape: its own copy of the script, its own virtual environment, and an integrations.json that stores the environment values as placeholders.

Step 1 · Create the Client B project folder

Client B runs its own copy. Create ~/<client-b-folder> and place server_single.py (Script B), pyproject.toml with the same dependencies, and .python-version containing 3.13 in it.

mkdir -p "$HOME/<client-b-folder>"
cd "$HOME/<client-b-folder>"
/opt/homebrew/bin/uv sync
Check: a .venv folder now exists inside the Client B folder. Unlike Script A, this copy uses its own environment, not the shared one.

Step 2 · Get Script B

Script B is Script A with the start-up code changed: no OAuth support, and it loads a .env from its own folder rather than using python-dotenv's default search. The easiest way to produce it is to copy Script A and apply the differences listed in section 13. Both copies expose the same 60 tool names.

Step 3 · Add one MCP server per instance in Client B

In the client application, add a stdio MCP server with this command, once per instance, with the three variables SN_INSTANCE, SN_USERNAME and SN_PASSWORD.

/opt/homebrew/bin/uv --directory ~/<client-b-folder> run server_single.py

Client B records each server in ~/Library/Application Support/<client-b>/integrations.json under mcp_servers. The env values are stored as placeholders of the form $MCP_ENV_<server-id>_SN_USERNAME, and the client substitutes the real value when it launches the process.

Two things to expect here. Menu labels vary by client version, so look for whatever the application calls adding a local or stdio MCP server. And where the client keeps the resolved value behind each $MCP_ENV_* placeholder is not documented, so treat the credential as present on the machine in some form and rotate it accordingly.

Step 4 · Confirm Client B started it

ps aux | grep <client-b-folder>
Check: a process running ~/<client-b-folder>/.venv/bin/python3 server_single.py whose parent is uv --directory ~/<client-b-folder> run server_single.py, started by the client application's bundled helper binary.

Script A and Script B side by side

AspectA: Claude CodeB: second desktop client
LauncherClaude CodeSecond desktop MCP client, through its bundled helper process
Project folder<project-root>/mcp-server~/<client-b-folder>
Script lengthAbout 2,534 linesAbout 2,478 lines
Virtual environment~/.venvs/mcp-server, shared, set with UV_PROJECT_ENVIRONMENT~/<client-b-folder>/.venv, its own
Commanduv --directory … run python server_single.pyuv --directory ~/<client-b-folder> run server_single.py
Credentials stored inClaude Code user config, as a plaintext env blockintegrations.json, as placeholders resolved at launch
OAuth password grantSupported when SN_CLIENT_ID and SN_CLIENT_SECRET are setNot supported, Basic Auth only
Dotenv loadingload_dotenv() with the default searchload_dotenv(Path(__file__).parent / '.env')
Two copies drift. Editing Script A does not change what Client B runs. After adding a tool to one, copy the change to the other if you want both to have it.

7. How Credentials Are Resolved

The environment from the client's config wins. The .env file is only a fallback.

flowchart TD
  A["Client starts the server process
with the env block from its config"] --> B["Environment now holds
SN_INSTANCE / SN_USERNAME / SN_PASSWORD"] B --> C["Script runs load_dotenv()"] C --> D{"Variable already set
in the environment?"} D -- "Yes" --> E["Environment value wins
.env is ignored for that variable"] D -- "No" --> F["Value is read from .env
if a .env file is found"] E --> G["os.getenv feeds ServiceNowClient"] F --> G G --> H{"Client id and secret
both set? Script A only"} H -- "Yes" --> I["OAuth password grant
POST /oauth_token.do"] H -- "No" --> J["Basic Auth
username and password"] classDef a fill:#e8f1ff,stroke:#3975c6,color:#132238; classDef g fill:#e8f7f1,stroke:#157a5a,color:#103e31; classDef y fill:#fff5dd,stroke:#9a6500,color:#5a3d00; class A,B,C,G a; class E,J g; class D,H,F,I y;

What this means in practice

  • Both launchers pass the three variables in the process environment, so the project-folder .env is never consulted for them. python-dotenv does not override variables that are already set.
  • The only .env in the project folder holds one instance and goes stale. It can be deleted without changing behaviour. Do not use it as the place to add new instances.
  • The instance value may be a full URL such as https://<instance>.service-now.com, a bare name such as <instance>, which becomes the full URL, or a host name. The script normalises all three.
  • Passwords are never written into the script. A search of Script A for hardcoded credentials and instance URLs found none.

8. How a Tool Call Works

Every tool is a Python function that calls ServiceNow's REST API and returns JSON text.

sequenceDiagram
  autonumber
  actor U as You
  participant C as AI client
  participant S as server_single.py · FastMCP over stdio
  participant K as ServiceNowClient
  participant N as ServiceNow instance
  U->>C: Show me the last 5 failed flow logs
  C->>S: tools/call sn_query_flow_logs · JSON over stdin
  S->>K: table_get on sys_flow_log with query and limit
  K->>N: GET /api/now/table/sys_flow_log · Basic Auth · 30 s timeout
  N-->>K: 200 with a result array
  K-->>S: success true, plus the data payload
  S-->>C: JSON text string of records
  C-->>U: Summary written from the records
    

The pieces inside server_single.py

PieceApproximate locationWhat it does
ServiceNowClientlines 16 to ~180Builds the base URL, holds one requests.Session, applies Basic Auth, or OAuth when a client id and secret are set, sets JSON headers and a 30 second timeout. Every response comes back as {success, status_code, data, error}.
table_get / table_create / table_update / table_deleteinside the clientGET, POST, PATCH and DELETE on /api/now/table/<table>. Reads set sysparm_exclude_reference_link=true and accept limit, offset, order and display value.
aggregateinside the clientCalls the Aggregate API, /api/now/stats/<table>, for counts and statistics.
batch_operationinside the clientLoops table_update or table_delete over a list of records and returns {success: [...], failed: [...]}.
client = ServiceNowClient(...)~line 166Reads the five environment variables once at start-up.
mcp = FastMCP("servicenow-<instance>")~line 174Creates the MCP server. Each function decorated with @mcp.tool() becomes a tool. The docstring becomes the description the model reads.
qh() helper~line 268Quick query helper used by most read tools. Caps the limit at 1000 and defaults to display values.
_resolve_agent_fields()~line 195Probes the sn_aia_agent columns once and caches the names, because the role and instructions columns differ between releases: role against agent_role, instructions against list_of_steps.
mcp.run(transport="stdio")end of fileStarts the server on stdin and stdout.
Guard rails in the code. Every delete tool, and batch delete, refuses to run unless confirm=True is passed. Most other writes have no guard, so the client's permission prompt is the safety net.

Three kinds of tool

1

Generic Table API wrappers

Query, get, create, update, delete and aggregate against any table you name.

Think: the raw platform, with a permission prompt in front of it.

2

Domain wrappers

The table and field names are fixed in the function, so the model does not have to know the AI agent object model to ask a useful question.

Think: a named question, not a table scan.

3

Tools with real logic

Multi-record operations such as sn_create_ai_agent, sn_create_rag_pipeline, sn_clone_ai_agent and sn_add_record_operation_tool.

Think: a small piece of automation behind one call.

Illustrative snippets

1. Reading the environment variables, which happens once, at import time:

import os
from dotenv import load_dotenv

load_dotenv()

# Basic Auth uses SN_INSTANCE / SN_USERNAME / SN_PASSWORD.
# For the optional OAuth password grant, also set SN_CLIENT_ID / SN_CLIENT_SECRET.
# When the client id and secret are both present, OAuth is used instead of Basic Auth.
client = ServiceNowClient(
    instance=os.getenv("SN_INSTANCE"),
    username=os.getenv("SN_USERNAME"),
    password=os.getenv("SN_PASSWORD"),
    client_id=os.getenv("SN_CLIENT_ID"),
    client_secret=os.getenv("SN_CLIENT_SECRET"),
)

mcp = FastMCP("servicenow-instance")
print("[OK] Connected", file=sys.stderr)

2. One representative tool definition and the helper almost every read tool calls:

@mcp.tool()
def sn_query(table: str, query: str = "", fields: str = "", limit: int = 20) -> str:
    """Query any table on the active instance."""
    return qh(table, query, fields, limit)


def qh(tbl: str, qry: str = "", flds: str = "", lmt: int = 20,
       ob: str = "-sys_created_on", dv: bool = True) -> str:
    """Quick helper for table queries."""
    fl = [f.strip() for f in flds.split(",")] if flds else None
    r = client.table_get(tbl, query=qry or None, fields=fl,
                         limit=min(lmt, 1000), order_by=ob,
                         display_value="true" if dv else "false")
    if r["success"]:
        return json.dumps({"count": len(r["data"].get("result", [])),
                           "records": r["data"].get("result", [])}, indent=2)
    return json.dumps({"error": r["error"]}, indent=2)

3. The Table API request helper:

def table_get(self, table: str, sys_id: str = None, query: str = None,
              fields: list = None, limit: int = 100, offset: int = 0,
              order_by: str = None, display_value: str = "false") -> dict:
    endpoint = (f"/api/now/table/{table}/{sys_id}" if sys_id
                else f"/api/now/table/{table}")
    params = {
        "sysparm_limit": limit,
        "sysparm_offset": offset,
        "sysparm_display_value": display_value,
        "sysparm_exclude_reference_link": "true",
    }
    if query:    params["sysparm_query"] = query
    if fields:   params["sysparm_fields"] = ",".join(fields)
    if order_by: params["sysparm_orderby"] = order_by
    return self._request("GET", endpoint, params=params)

4. The single _request method every verb funnels through, which is what produces the uniform response envelope:

def _request(self, method, endpoint, params=None, data=None) -> dict:
    url = f"{self.base_url}{endpoint}"
    try:
        response = self.session.request(method=method, url=url, params=params,
                                        json=data, timeout=self.timeout)
        return {
            "success": response.ok,
            "status_code": response.status_code,
            "data": response.json() if response.text else None,
            "error": None if response.ok
                     else f"HTTP {response.status_code}: {response.reason}",
        }
    except Exception as e:
        return {"success": False, "status_code": None, "data": None, "error": str(e)}
The real method does a little more. On the OAuth path it fetches a bearer token before the call, and on a 401 with a refresh token present it refreshes once and retries.

9. Tool Catalogue

All 60 tools in Script A, grouped by purpose. The tool names are identical in Script B.

Every tool name starts with the same prefix on every instance. In the client, the full name adds a server prefix, for example mcp__servicenow-sn-<instance-b>__sn_query, and that server part is what tells you which instance answered.

Generic data access (8)

These work on any table. They are thin Table API wrappers.

Tool nameTypeWhat it does
sn_queryreadQuery any table on the active instance.
sn_get_recordreadGet a single record by sys_id from the instance.
sn_create_recordwriteCreate a new record in the instance.
sn_update_recordwriteUpdate an existing record in the instance.
sn_delete_recorddelete, confirm requiredDelete a record from the instance. Requires confirmation.
sn_aggregatereadGet aggregated statistics from the instance.
sn_batch_updatewriteUpdate multiple records in the instance.
sn_batch_deletedelete, confirm requiredDelete multiple records from the instance. Requires confirmation.

Logs and diagnostics (9)

Read syslog, Flow Designer, generative AI logs and RAG index health.

Tool nameTypeWhat it does
sn_query_syslogreadQuery the ServiceNow syslog table for debugging.
sn_query_syslog_app_scopereadQuery syslog_app_scope, the scoped log view accessible without cross-scope privilege.
sn_query_flow_contextsreadQuery Flow Designer execution contexts, sys_flow_context.
sn_query_flow_logsreadQuery Flow Designer detailed logs, sys_flow_log.
sn_get_flow_context_detailsreadGet complete details of a flow execution including its logs.
sn_query_generative_ai_logs_detailedreadQuery sys_generative_ai_log with full field access for detailed AI and LLM debugging.
sn_query_flow_reportsreadQuery Flow Designer reporting data, sys_flow_report_doc_chunk.
sn_query_generative_ai_logsreadQuery generative AI logs, sys_generative_ai_log.
sn_check_rag_index_healthreadCheck RAG indexing status via ais_ingest_table_stats.

AI agent inspection (15)

Read agents, tools, workers, triggers and execution history on the sn_aia_* tables.

Tool nameTypeWhat it does
sn_list_agentic_workflowsreadList all agentic workflows, the use cases, configured in the system.
sn_list_ai_agentsreadList all AI agents configured in the system.
sn_get_agent_detailsreadGet detailed information about a specific AI agent including its tools.
sn_list_agent_toolsreadList all tools available to AI agents.
sn_list_agent_tool_m2msreadList all tool M2M records wired to a specific agent, sn_aia_agent_tool_m2m.
sn_query_execution_plansreadQuery AI agent execution plans, sn_aia_execution_plan.
sn_query_execution_tasksreadQuery AI agent execution tasks, sn_aia_execution_task.
sn_query_tool_executionsreadQuery tool executions to see which tools were called and what they returned.
sn_get_execution_detailsreadGet complete details of an agentic workflow execution including all tasks and tool calls.
sn_query_agent_messagesreadQuery AI agent conversation messages, sn_aia_message.
sn_list_trigger_configurationsreadList trigger configurations for agentic workflows.
sn_list_agent_child_mappingsreadList agent child mappings, sn_aia_agent_child, which is the orchestration tree.
sn_list_ai_workersreadList all AI Workers configured in the system.
sn_get_ai_worker_detailsreadGet full details of a specific AI Worker including its template and mapped users.
sn_list_worker_templatesreadList all AI Worker Templates in the system.

AI agent authoring (19)

Create, change and delete agents, workflows, tools, triggers and parent-to-child mappings.

Tool nameTypeWhat it does
sn_create_ai_agentwriteCreate a new AI agent.
sn_update_ai_agentwriteUpdate an existing AI agent. Only provide the fields you want to update.
sn_delete_ai_agentdelete, confirm requiredDelete an AI agent. Requires confirmation.
sn_add_tool_to_agentwriteAdd a tool to an AI agent, with optional input definitions.
sn_remove_tool_from_agentwriteRemove a tool from an AI agent.
sn_create_agentic_workflowwriteCreate a new agentic workflow, or use case.
sn_update_agentic_workflowwriteUpdate an existing agentic workflow. Only provide the fields you want to update.
sn_delete_agentic_workflowdelete, confirm requiredDelete an agentic workflow. Requires confirmation.
sn_create_toolwriteCreate a new tool for AI agents.
sn_add_record_operation_toolwriteCreate and attach a fully configured Record Operation, or CRUD, tool to an AI agent.
sn_update_toolwriteUpdate an existing tool. Only provide the fields you want to update.
sn_delete_tooldelete, confirm requiredDelete a tool. Requires confirmation.
sn_create_triggerwriteCreate a trigger for an agentic workflow.
sn_update_triggerwriteUpdate an existing trigger. Only provide the fields you want to update.
sn_delete_triggerdelete, confirm requiredDelete a trigger. Requires confirmation.
sn_clone_ai_agentwriteClone an existing AI agent with all its tools and configuration.
sn_cleanup_agent_configswriteClean up duplicate agent config records for an agent.
sn_create_agent_child_mappingwriteCreate a parent-to-child agent mapping, sn_aia_agent_child.
sn_delete_agent_child_mappingdelete, confirm requiredDelete an agent child mapping, removing a parent-to-child delegation relationship.

AI Worker management (8)

Create and change AI Workers and Worker Templates, and map users to workers.

Tool nameTypeWhat it does
sn_create_ai_workerwriteCreate a new AI Worker and attach a Worker Template to it.
sn_update_ai_workerwriteUpdate an existing AI Worker. Only provide the fields you want to change.
sn_delete_ai_workerdelete, confirm requiredDelete an AI Worker. Requires confirmation.
sn_assign_user_to_workerwriteMap a ServiceNow user to an AI Worker via sn_aia_worker_user_m2m.
sn_remove_user_from_workerdelete, confirm requiredRemove a user mapping from an AI Worker, deleting the sn_aia_worker_user_m2m record.
sn_create_worker_templatewriteCreate a new AI Worker Template and associate it with a root agent.
sn_update_worker_templatewriteUpdate an existing Worker Template. Only provide the fields you want to change.
sn_delete_worker_templatedelete, confirm requiredDelete a Worker Template. Requires confirmation.

RAG pipeline (1)

Build a complete RAG search pipeline and attach it to an agent. This one tool carries about 300 lines of logic.

Tool nameTypeWhat it does
sn_create_rag_pipelinewriteCreate a complete RAG search pipeline and wire it to an AI agent.
Totals: 8 + 9 + 15 + 19 + 8 + 1 = 60 tools per server, on every instance you register.

10. Verification Checklist

Run these in order. Stop at the first one that fails and go to Troubleshooting.

#CheckCommand or actionExpected result
1Python and uv/opt/homebrew/bin/uv --version and ~/.venvs/mcp-server/bin/python --versionuv 0.x prints a version, and Python prints 3.13.x
2Dependencies~/.venvs/mcp-server/bin/python -c "import mcp, requests, dotenv"No output and no error
3Instance reachablecurl -sI https://<instance>.service-now.com | head -1An HTTP status line, such as 200 or 302
4Credentials validcurl -s -o /dev/null -w '%{http_code}' -u '<username>:<password>' 'https://<instance>.service-now.com/api/now/table/sys_user?sysparm_limit=1'200. A 401 means wrong credentials, a 403 means a missing role
5Script startsStep 3 of section 5Banner with [OK] Connected, and the process stays running
6Servers registeredclaude mcp listEvery server you added listed as connected
7Tools visibleAsk the client to list the ServiceNow tools60 tools per server
8End to endAsk the client to query sys_user with limit 1 on each serverA record is returned from each instance
9Client B copy runningps aux | grep <client-b-folder>A process running ~/<client-b-folder>/.venv/bin/python3 server_single.py

11. Troubleshooting

The failures you are most likely to hit, and what each one actually means.

SymptomLikely causeFix
Server shows as failed in claude mcp listuv not found, wrong --directory, or Python 3.13 missingRun step 3 of section 5 by hand and read the error. Use the absolute path /opt/homebrew/bin/uv in the config, because GUI applications do not inherit your shell PATH.
A folder path with spaces breaksThe project path contains spacesKeep the path as a single quoted argument. In JSON it is already one string in the args array. In the shell, wrap it in double quotes.
Tool returns HTTP 401: UnauthorizedWrong username or password, or Basic Auth is blocked for the accountRe-run the credentials check, row 4 of section 10. Update the env block in the config and restart.
Tool returns HTTP 403: ForbiddenThe user lacks a role for that table or operationGrant the role, or use a user that has it. Reads of sn_aia_* tables need AI Agent roles or admin.
Tool returns HTTP 404, or an empty result for sn_aia_* tablesThe AI Agent Studio plugins are not installed on that instanceCheck the table exists in sys_db_object on the instance.
Banner says Connected but every call failsThe banner is printed without ever calling the instanceUse the credentials check to find the real error.
Changes to the script are not picked upThe process is still the old oneRestart the client so it relaunches the server. Client B runs its own copy, so edit ~/<client-b-folder>/server_single.py for it.
Tools from the wrong instance answeredEvery server exposes the same 60 tool namesLook at the server part of the full tool name, mcp__servicenow-sn-<name>__, to see which instance answered.
Timeout errors on large queriesThe 30 second request timeout, and the 1000-row cap in the quick query helperNarrow the query, lower the limit, or raise self.timeout in ServiceNowClient.
Python or package errors after a uv upgradeStale virtual environmentDelete ~/.venvs/mcp-server and re-run uv sync, step 2 of section 5.

12. Security Hardening

The setup works as described. These are the risks it carries, and how to reduce them.

Current state. The password for each Claude Code server sits in plaintext in the Claude Code user config. Client B keeps only placeholders in integrations.json, and where it stores the resolved values is not documented. Servers that run as an admin user give every tool call, including the delete and batch tools, full admin rights on the instance. Servers on service accounts can do only what those accounts' roles allow.

Recommended changes

1. A dedicated service user per instance

Give it only the roles the tools need, instead of admin. A read-only user is enough if you only use the query and inspection tools.

2. Prefer OAuth where possible

Script A supports the OAuth password grant when SN_CLIENT_ID and SN_CLIENT_SECRET are set. It lets you revoke access without changing the user's password. Script B does not support it.

3. Keep secrets out of synced folders

Do not put a .env holding credentials in a cloud-synced project folder. It is copied to the cloud and to your other devices.

4. Keep write tools behind prompts

Only allow-list the read tools in settings.json. Leave create, update and delete tools to prompt every time.

5. Rotate in every place

When a password changes, update each location that stores it: the matching entry in the Claude Code user config, the matching server's env values in Client B, and any other tool holding the same credential.

6. Do not share the config files

They are personal. Share this guide, which contains no credentials, instead.

13. Known Issues in This Setup

Things that work today but will surprise the next person who opens the folder.

IssueDetailSuggested action
Two diverged copies of the scriptScript B is about 56 lines shorter than Script A: no OAuth code, and it loads a .env from its own folder instead of using the default search. The 60 tool names are the same.Pick one as the source of truth and copy changes across, or point both launchers at one file.
Unused .envThe project-folder .env contains one instance only, and is overridden by the env blocks.Delete it, or keep it only as a documented fallback.
Instance names inside tool docstringsA docstring that names one particular instance is misleading on every other instance, and the model reads these descriptions.Replace with neutral wording such as "the instance".
Misleading start-up bannerIt prints a fixed instance name and [OK] Connected for every instance, and it never calls the instance.Print the real SN_INSTANCE value, and make one test call.
Legacy swap scriptsswap_instance.sh lists two hardcoded instance names and rewrites server_single.py with sed.Do not use it with this setup. It conflicts with the env block approach.
Dead editor and Copilot MCP filesAn editor mcp.json points at an old project path and has no credentials. A separate Copilot mcp.json targets a conda environment.Remove or update them if you do not use those clients.
Dead client settingsenabledMcpjsonServers in ~/.claude/settings.json names two servers, but ~/.mcp.json is empty. The allow list also holds a stale tool entry from a renamed server.Clean up the settings.
Extra copies of the scriptFurther copies of server_single.py exist in other local folders. Nothing launches them.Archive or delete them, to avoid editing the wrong file.
Plaintext credentials in two placesThe Claude Code user config holds the passwords for every Claude Code server, and the unused legacy .env holds one more. See section 12.Apply the hardening changes.

14. Comparison with the Native MCP Server Console

ServiceNow's own way to expose tools to an AI client. A complement to this setup, not a replacement for it.

AspectThis FastMCP setupNative MCP Server Console
Adding a toolWrite one Python function in server_single.pyBuild a backing implementation first, for example a Scripted REST API with an OpenAPI schema, create the tool in the console, then add it to a server record
IdentityOne shared credential per instance, admin on dev instances and service accounts elsewherePer-user OAuth. The user consents once, and tools run with that user's permissions and ACLs
ReachAny instance reachable over HTTPS, from one scriptOne server record per instance, and it needs the sn_mcp_server plugin
Best forBuilding and debugging AI agents, flows and execution plans, which is platform authoring workGoverned, narrow business tools such as incident read and update
HostingA local process on your machine, over stdioHosted on the instance. The client connects to a Server URL as a custom connector
Audit and controlWhatever your own discipline gives youServers and tools are records on the instance, so update sets and reviews apply

Platform baseline assumed here

  • Both instances in this example run a recent Australia patch with the Model Context Protocol Server plugin (sn_mcp_server) and the client plugin (sn_mcp_client) installed.
  • Each instance already had several provisioned MCP servers in sn_mcp_server_registry, for example sn_itsm_mcp_server.itsm_default, sn_hr_mcp_server.hr_default and sn_csm_mcp_server.csm_default.
  • The Dev Program series this appendix follows assumes a recent Australia patch with the plugin installed, an OAuth Application Registry entry whose redirect URL is https://claude.ai/api/mcp/auth_callback, Token Format JWT and the useraccount scope. The authenticating user needs itil or an equivalent role on incident.

Which one to reach for

Building and debugging agents

Use the FastMCP setup. You need broad table access across several instances, and you are the only user.

Reading and updating business records

Use the native console, so calls run under a real user's permissions rather than as admin.

Anything you hand to someone else

Use the native console. The server, its tools and the consent are records on the instance, which is what makes the access reviewable.

15. Appendix: the Native MCP Server Console, Step by Step

ServiceNow hosts the MCP server on the instance and the client connects to it with OAuth. No local Python process is involved.

Source. This walkthrough follows the ServiceNow Dev Program series by Travis Toulson, namely Build MCP Tools in the MCP Server Console, Create the MCP Server Record in ServiceNow, Connect Claude to a ServiceNow MCP Server and Claude Reads and Updates a Real ServiceNow Incident, together with the written ServiceNow Community article Building an MCP Server on ServiceNow and Connecting Claude to It. Check the scripts below against the originals before using them on a customer instance.

flowchart TD
  U["You"] --> CL["AI client
custom connector"] CL -- "OAuth 2.0 consent
scope useraccount" --> OA["OAuth Application Registry
MCP connector entry, JWT tokens"] CL -- "HTTPS MCP calls
to the Server URL" --> MS["MCP Server record
Incident MCP Server"] MS --> T1["Tool: Get Incident"] MS --> T2["Tool: Update Incident"] T1 -- "GET /incident_api by number" --> API["Scripted REST API
incident_api"] T2 -- "POST /incident_api by number
validated by UpdateIncidentSchema" --> API API --> GR["GlideRecord on incident
runs as the consenting user"] classDef a fill:#e8f1ff,stroke:#3975c6,color:#132238; classDef b fill:#fff5dd,stroke:#9a6500,color:#5a3d00; classDef c fill:#f1eaff,stroke:#7b52b9,color:#2f1f4b; classDef d fill:#e8f7f1,stroke:#157a5a,color:#103e31; class U,CL a; class OA b; class MS,T1,T2 c; class API,GR d;

Step 1 · Check the instance and install the plugin

You need a recent Australia patch, the Now Assist Admin Console plugin (sn_nowassist_admin, listed as AI Admin Hub on newer instances) and the admin role. Install Model Context Protocol Server (sn_mcp_server) from All > System Applications > All Available Applications > All.

curl https://<instance>.service-now.com/sncapps/mcp-server/health
Check: the response is {"status":"healthy"}. In the filter navigator, sys_service.list should show the MCP-S and mcp-server records. Then create a scoped application in Studio, which the article calls "Incident MCP Server", to hold everything below.

Step 2 · Create the Scripted REST API and the GET resource

Go to All > System Web Services > Scripted REST APIs. Create an API named Incident API with API ID incident_api and Default ACLs Scripted REST External Default. Add a resource Get Incident: method GET, relative path /{number}, the same default ACLs, with this script.

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
  var number = request.pathParams.number;
  if (!number) {
    response.setStatus(400);
    response.setBody({ message: "Incident number is required." });
    return;
  }
  var gr = new GlideRecord('incident');
  gr.addQuery('number', number);
  gr.query();
  if (gr.next()) {
    response.setStatus(200);
    response.setBody({
      sys_id: gr.getValue('sys_id'),
      number: gr.getValue('number'),
      short_description: gr.getValue('short_description'),
      description: gr.getValue('description'),
      state: gr.getValue('state'),
      urgency: gr.getValue('urgency'),
      priority: gr.getValue('priority'),
      assigned_to: gr.getDisplayValue('assigned_to'),
      assignment_group: gr.getDisplayValue('assignment_group'),
      opened_at: gr.getValue('opened_at'),
      caller_id: gr.getDisplayValue('caller_id')
    });
  } else {
    response.setStatus(404);
    response.setBody({ message: "Incident not found: " + number });
  }
})(request, response);
Check: the resource saves with no script errors.

Step 3 · Add the POST resource and its OpenAPI schema

On the same API add a resource Update Incident: method POST, relative path /{number}, the same default ACLs, with this script.

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
  var number = request.pathParams.number;
  if (!number) {
    response.setStatus(400);
    response.setBody({ message: "Incident number is required." });
    return;
  }
  try {
    var params = request.body.data;
    var gr = new GlideRecord('incident');
    gr.addQuery('number', number);
    gr.query();
    if (!gr.next()) {
      response.setStatus(404);
      response.setBody({ message: "Incident not found: " + number });
      return;
    }
    for (var key in params) {
      if (params.hasOwnProperty(key)) {
        if (key === 'work_notes' || key === 'comments') {
          gr[key] = params[key];
        } else {
          gr.setValue(key, params[key]);
        }
      }
    }
    gr.update();
    response.setStatus(200);
    response.setBody({
      message: "Incident updated successfully.",
      number: gr.getValue('number'),
      sys_id: gr.getUniqueValue()
    });
  } catch (e) {
    response.setStatus(500);
    response.setBody({ message: "Error occurred: " + e.message });
  }
})(request, response);

In the API's Schemas related list create UpdateIncidentSchema, OpenAPI version 3.0.1, with this JSON, then link it to the POST resource through the Request Schema related list. This is what gives the tool its input fields.

{
  "type": "object",
  "description": "Fields to update on the incident. All fields are optional. Include only what you want to change.",
  "properties": {
    "short_description": { "type": "string", "description": "Brief summary of the incident.", "example": "User cannot log in to VPN" },
    "description": { "type": "string", "description": "Detailed description of the incident.", "example": "User reports being unable to connect to VPN since this morning." },
    "urgency": { "type": "string", "description": "Urgency code. 1=High, 2=Medium, 3=Low.", "example": "2" },
    "impact": { "type": "string", "description": "Impact code. 1=High, 2=Medium, 3=Low.", "example": "2" },
    "state": { "type": "string", "description": "Incident state code. 1=New, 2=In Progress, 6=Resolved, 7=Closed.", "example": "2" },
    "assignment_group": { "type": "string", "description": "sys_id or name of the group to assign the incident to.", "example": "Service Desk" },
    "assigned_to": { "type": "string", "description": "sys_id or username of the person to assign the incident to.", "example": "<username>" },
    "work_notes": { "type": "string", "description": "Internal notes to add to the incident work notes journal. Visible to agents only.", "example": "Investigated and escalated to network team." },
    "comments": { "type": "string", "description": "Customer-visible notes to add to the incident comments journal. Visible to the caller and agents.", "example": "We are investigating your issue and will update you shortly." }
  }
}
Check: the POST resource shows UpdateIncidentSchema under Request Schema.

Step 4 · Register the OAuth client

Go to All > System OAuth > Application Registry and create a new entry. On the interceptor page choose [Deprecated UI] Create an OAuth API endpoint for external clients, which is still the right choice despite the label.

  • Name: MCP connector entry
  • Redirect URL: https://claude.ai/api/mcp/auth_callback, exactly, with no trailing slash
  • Token Format: JWT, not the default Opaque
  • Auth Scopes: add useraccount

Save, then copy the Client ID (<client-id>) and Client Secret (<client-secret>).

Check: the saved record shows Token Format JWT.

Step 5 · Create the two MCP tools

Go to All > Admin Center > MCP Server Console > Tools > Create Tool > REST API. In the endpoint search, type /x to filter to scoped endpoints.

  • Select [GET] /x_snc_<scope>/incident_api/{number}. Label Get Incident. Description: Retrieves the details of a specific ServiceNow incident by incident number. Call this tool when the user wants to look up, check, or get information about a specific incident.
  • Select [POST] /x_snc_<scope>/incident_api/{number}. Label Update Incident. Description: Updates fields on an existing ServiceNow incident by incident number. Call this tool when the user wants to change, modify, or update an incident. Pass only the fields you want to change along with the required incident number.
The Label and Description are what the model reads to decide when to call the tool, so write them for the model, not for a form.
Check: the GET tool has one required input, number. The POST tool has number plus all the schema fields.

Step 6 · Create and activate the MCP server

In the console open Servers > Create Server. Label Incident MCP Server. Short description: Access and manage ServiceNow incidents by incident number. Use this server to retrieve incident details or update incident fields including urgency, state, assignment, and work notes. Click Add Tools, select both tools, then Create. Click Activate; if the button reads Deactivate it is already active.

Check: copy the Server URL from the record. You need it in the next step.

Step 7 · Connect the AI client

In Claude.ai, go to the profile icon, then Settings > Connectors > Add > Add custom connector. Name it, for example ServiceNow Incident MCP. The Remote MCP Server URL is the Server URL, and the OAuth Client ID and Client Secret are the values from step 4. Click Add, then Connect. Sign in to ServiceNow and approve the consent screen for the connector with the useraccount scope.

Check: the connector status changes to Connected and lists two tools. The consent screen plus the Application Registry entry are your answer to "what can this AI actually do".

Step 8 · Test a read and a write

Open a new chat with the connector enabled and use a test incident number.

Get the details of ServiceNow incident <incident-number>.

Update incident <incident-number>. Set urgency to High and add a work note
that says "Escalated to network team for investigation."
Check: open the incident in ServiceNow and confirm that the urgency and the work note match. The client's confirmation only shows what the model believes happened. The record is the proof.

Appendix troubleshooting

SymptomCauseFix
Connector shows zero toolsToken Format is Opaque instead of JWTChange it to JWT in the Application Registry, then disconnect and reconnect
Tool calls return errorsThe authenticating user lacks access to incidentGive the user itil or an equivalent role. The useraccount scope delegates the user's permissions, it does not add new ones
OAuth flow failsThe redirect URL differs from https://claude.ai/api/mcp/auth_callback, for example by a trailing slashCorrect it exactly. Being routed through SSO during the flow is normal
Nothing worksPlugin or service unhealthyRe-run the health check from step 1
Differences from the FastMCP setup. Each tool needs a backing implementation, here a Scripted REST API, and calls run as the consenting user under ACLs rather than as admin. The series author notes that later parts back tools with flow actions, Now Assist skills, knowledge graphs and subflows.

16. Quick Decision Guide

Five questions that place you in the right section of this guide.

FastMCP setup

“I am building and debugging AI agents, and I need broad table access across several instances.”

Use this setup. Register the same command once per instance with its own SN_INSTANCE, SN_USERNAME and SN_PASSWORD, as in section 5, and read the server part of each tool name to know which instance answered.

Native console

“Someone else will use this against real business records.”

Use the native MCP Server Console instead, as sections 14 and 15 describe. Calls then run under the consenting user's permissions and ACLs, and the server, its tools and the consent are all records on the instance.

Troubleshooting

“My server shows as failed, or every tool call returns an error.”

Run the script by hand with the three variables, step 3 of section 5, and read the error. A start-up banner is not proof of a working credential, so confirm with the curl check in row 4 of section 10 before you change anything else.

Two copies

“I added a tool, but one of my clients still cannot see it.”

The two clients run two separate copies of the script. Edit the copy that client launches, then restart the client so it relaunches the process. Sections 6 and 13 cover the drift and what to do about it.

Credentials

“Where do the passwords live, and what has to change when one is rotated?”

In the client configuration, in plaintext, and the process environment always wins over any .env file. Section 7 traces the resolution order, and section 12 lists every place a rotated credential has to be updated.