
papamzor/personal-mcp-servicenow
Developer toolsStarted as personal project but evolved into something used widely at my workplace and beyond. MCP server for ServiceNow
Overview
MCP server for ServiceNow integration. Uses FastMCP over stdio transport, OAuth 2.0 client credentials. Current release: (25 tools). The 5.0.0 "Boron" surface was breaking — culled 39 → 25 with one minimal response contract. If upgrading from v4, read MIGRATION_v4_to_v5.md. Full history in CHANGELOG.md. † The shipped version string went 4.0.0 → 4.3.0 directly. Neither 4.1.0 nor 4.2.0 was ever released or tagged — "4.1.0" labels a body of work in the CHANGELOG, not an installable version. v4.0 was a breaking release. See MIGRATION_v3_to_v4.md if upgrading from v3. Backwards-compat shims kept all v3 import paths and test-patch targets working for one release cycle; they were deleted in 4.1.0. Import from http_layer, oauth, and filter directly. This works on Windows 11 and macOS and takes about 2 minutes. ask your IT department for 3 things: - Your company's ServiceNow address (looks like https://company.service-now.com) - A Client ID - A Client Secret 1.
README
Personal MCP ServiceNow Integration
MCP server for ServiceNow integration. Uses FastMCP over stdio transport, OAuth 2.0 client credentials.
Support This Project
Release highlights
Current release: 5.0.2 (25 tools). The 5.0.0 “Boron” surface was breaking — culled 39 → 25 with one minimal response contract. If upgrading from v4, read MIGRATION_v4_to_v5.md. Full history in CHANGELOG.md.
| Release | What changed |
|---|---|
| 5.0.2 | readOnlyHint served on all 25 tools (19 read / 6 write) so hosts can auto-approve reads. Dependencies locked: uv.lock shipped in the .mcpb (uv run --frozen --no-dev), fastmcp<4 ceiling (4.0.x deferred), direct pins pruned 20 → 6, pip-audit clean. No tool-surface change. |
| 5.0.1 | Two silent-data-loss fixes in get_kb_articles_by_state, both found live. A draft filter now finds drafts on already-published articles (the priority collapse hid them — live, 1 reported against 48 real); entries carry states_present and the state filter tests membership. The raw scan no longer inherits max_results, which had made a truncated fetch report the wrong current_state; a capped scan now says scan_incomplete instead of guessing. |
| 5.0.0 “Boron” | Breaking. Tool surface culled 39 → 25 (5 diagnostics folded into one health_check; the NL/filter and smart-KB/SLA read tools removed — the host model does NL→filter natively). One minimal response contract across every tool: list {result, returned_count, truncated}, single-record {record}, write {record, message}, failure {error:{code, message}} — no more bare-string returns or result-is-sometimes-a-dict. TableSpec makes per-table config one source of truth; tool selection guidance is a structured registry injected into each docstring. ~2000 lines of dead NL-engine code removed. See MIGRATION_v4_to_v5.md. |
| 4.5.0 | Tool-selection docstring protocol on all 39 tools (WHEN TO USE / WHEN NOT TO USE / PREFER OVER / TABLES / SIDE EFFECT / EXAMPLE). Non-breaking — no tool added, removed, or re-signatured. The fatal footguns (LIKE-not-CONTAINS, reference fields hold sys_ids) now sit inline on search_records and filter_records. Static tool-selection preferred-hit rose 21/30 → 29/30, ambiguity 66 → 50 plausible paths. |
| 4.4.1 | Encoded-query values are carried faithfully. A & or a literal %XY in a search value no longer silently changes the query into a broader one; a ^ is refused rather than answered, because ServiceNow’s syntax cannot carry it inside a value. A KB title containing & or % no longer blocks a publish. |
| 4.4.0 | Correctness release. A failed read is no longer reported as a missing record — reads raise a classified error instead of returning None, so a timeout, an expired credential and an empty table stop producing the same answer. KB publishing is fail-closed on an unusable duplicate check. Legacy domain filtering deleted, so result sets get larger. See the CHANGELOG’s “Behavior changes” before upgrading. |
| 4.3.0 | Claude Desktop .mcpb packaging; Nuitka binary builds dropped from the release workflow. Absorbed the performance and token work planned as 4.2 (pooled httpx client, one OR-combined keyword query, concurrent CMDB probes). |
| 4.1.0 † | v4.0 shims deleted; KB write tools; get_kb_articles_by_state; get_query_syntax_help; filter_records truncation metadata. |
| 4.0.0 | SLA collapse, filter/ + http_layer/ + oauth/ packages. Breaking — see below. |
† The shipped version string went 4.0.0 → 4.3.0 directly. Neither 4.1.0 nor 4.2.0 was ever released or tagged — “4.1.0” labels a body of work in the CHANGELOG, not an installable version.
v4.0, in detail
v4.0 was a breaking release. See MIGRATION_v3_to_v4.md if upgrading from v3.
| Change | Detail |
|---|---|
| SLA tool consolidation | 8 tools collapsed into 3 (query_slas_by_task, query_slas_by_status, query_slas_custom). Tool count: 37 → 32. |
get_sla_details bug fix |
v3 built a number={sys_id} filter on task_sla (no number field). ServiceNow ignored it, returning 10,000 rows (~1.2M tokens). v4 routes via sys_id= — single record (~69 tokens). |
filter/ package |
Filter construction, validation, NL parsing, and explanation consolidated into a single package. query_validation.py and query_intelligence.py became shims, deleted in 4.1.0. |
http_layer/ + oauth/ packages |
make_nws_request and ServiceNowOAuthClient split into focused packages. GET-path token-optimization invariants locked by 3 negative write-path tests. |
Backwards-compat shims kept all v3 import paths and test-patch targets working for one release cycle; they were deleted in 4.1.0. Import from http_layer, oauth, and filter directly.
Easy install — Claude Desktop (no technical knowledge needed)
This works on Windows 11 and macOS and takes about 2 minutes.
Before you start: ask your IT department for 3 things:
- Your company’s ServiceNow address (looks like
https://company.service-now.com) - A Client ID
- A Client Secret
Steps:
- Open the Releases page and download the file ending in
.mcpb. - Double-click the downloaded file. Claude Desktop will open an install prompt — click Install. (If double-clicking doesn’t open Claude Desktop, go to Claude Desktop → Settings → Extensions → Advanced settings → Install Extension… and pick the file instead.)
- Fill in the three boxes with the values your IT department gave you, then save.
- Start a new chat and try asking: “Show me my open ServiceNow incidents”.
First use note: the first chat after installing can take a minute while the extension sets itself up. This only happens once.
Something not working?
- “Server disconnected” — double-check the three values you entered (Claude Desktop → Settings → Extensions → ServiceNow for Claude → Configure).
- Ask Claude to “test the ServiceNow connection” — it will tell you what’s wrong.
- On a company network, the one-time setup needs access to
pypi.org,files.pythonhosted.organdastral.sh. Ask IT to allow these if setup doesn’t finish.
IT readers: see docs/MCPB_BUILD.md for build, release and proxy details.
Installation (developers)
From source
git clone https://github.com/Papamzor/personal-mcp-servicenow.git
cd personal-mcp-servicenow
uv sync # reproducible install from uv.lock (runtime + dev group)
Without uv:
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements-dev.txt
Docker (cloud / network agents)
For hosting over the network (N8N, LangChain, any MCP-compatible agent):
docker build -t mcp-servicenow .
docker run -d \
-p 8000:8000 \
--env-file .env.local \
--name mcp-servicenow \
mcp-servicenow
Do not pass secrets with
-e KEY=value. They land in shell history and are visible viaps/docker inspect. Use--env-filefor local testing; see Production: Azure Container Apps + Key Vault for production.
The image sets MCP_TRANSPORT=sse by default. Agents connect at http://:8000/sse.
Configuration
Create .env in project root:
SERVICENOW_INSTANCE=https://your-instance.service-now.com
SERVICENOW_CLIENT_ID=your_oauth_client_id
SERVICENOW_CLIENT_SECRET=your_oauth_client_secret
OAuth 2.0 client credentials are required. Basic auth is not supported. See OAUTH_SETUP_GUIDE.md for ServiceNow-side setup.
Claude Desktop / Claude Code integration
Claude Desktop — config file location:
- Windows:
%APPDATA%\Claude\claude_desktop_config.json - macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"servicenow": {
"command": "python",
"args": ["/full/path/to/personal-mcp-servicenow/personal_mcp_servicenow_main.py"]
}
}
}
Credentials are read from the .env file. If you prefer to inject them via the config:
{
"mcpServers": {
"servicenow": {
"command": "python",
"args": ["/full/path/to/personal-mcp-servicenow/personal_mcp_servicenow_main.py"],
"env": {
"SERVICENOW_INSTANCE": "https://your-instance.service-now.com",
"SERVICENOW_CLIENT_ID": "your_oauth_client_id",
"SERVICENOW_CLIENT_SECRET": "your_oauth_client_secret"
}
}
}
}
Claude Code (remote SSE):
{
"mcpServers": {
"servicenow": {
"type": "sse",
"url": "http://:8000/sse"
}
}
}
Available tools (25)
v5.0 “Boron” (Tier 2 cull, 39 → 25), deletes-only. Removed the redundant
get_record_summary; the three smart-KB read tools;similar_slas_for_text; the five NL/filter tools (the host model builds and explains filters natively); and folded the five diagnostics into onehealth_check.
Generic table tools (4)
Work across all supported tables: incident, change_request, sc_req_item, sc_task, universal_request, kb_knowledge, vtb_task, task_sla.
search_records(table, query)— text similarity searchget_record(table, number)— full detail fields for a single recordfind_similar(table, number)— records similar to an existing recordfilter_records(table, filters, fields=None, max_results=100)— field-value filters with operators and date ranges; response carriesreturned_count/truncated
Query-syntax help (1)
get_query_syntax_help()— encoded-query operator reference (LIKE vs CONTAINS, reference-field dot-walking)
Priority incidents (1)
get_priority_incidents(priorities, start_date, end_date, additional_filters, include_metadata)
Knowledge base — read (1)
get_kb_articles_by_state(workflow_state, category, kb_base, max_results)— collapses ServiceNow KB versioning to one row pernumber, withcurrent_state(the canonical/live state),states_present(every state that number has a version in) andversion_count.workflow_statematches onstates_presentmembership, so a draft on an already-published article is found.max_resultscaps the returned entries; the raw scan runs to its own 1000-row ceiling and setsscan_incomplete+warningif it hits it. For topic search usesearch_records("kb_knowledge", ...); for a category usefilter_records("kb_knowledge", {"kb_category": ...}).
Knowledge base — write (5)
update_knowledge_article(article_number, update_data)publish_knowledge_article(article_number)— fire-and-verify; a POST is not trusted as truthpublish_knowledge_articles(article_numbers, concurrency)— batch, capped at 20 numbers, flat per-article status rowsretire_knowledge_article(article_number)check_kb_duplicates(article_numbers, concurrency)— the publish-time duplicate check, standalone
Private task CRUD (2)
create_private_task(task_data)— creates vtb_task recordupdate_private_task(task_number, update_data)— PATCH update
SLA management (4)
get_sla_details(sla_sys_id)query_slas_by_task(task_number)query_slas_by_status(status, days?, threshold_minutes?, stage?, extra_filters?)— status enum:"active","breached","breaching","critical","by_stage","performance"query_slas_custom(filters, fields?, days?)— escape hatch;fields=Nonedefaults toESSENTIAL_FIELDS["task_sla"]
CMDB (6)
find_cis_by_type(ci_type, detailed=False)— anycmdb_ci*tablesearch_cis_by_attributes(name=None, ip_address=None, location=None, status=None, ci_type=None, detailed=False)— at least one attribute requiredget_ci_details(ci_number, ci_type=None)— probes the common CI tables whenci_typeis omittedsimilar_cis_for_ci(ci_number)get_all_ci_types()— livesys_db_objectquery, not a static listquick_ci_search(search_term)
Diagnostic (1)
health_check(probe_table=None)— server liveness + auth config + live ServiceNow probe;probe_tablealso returns that table’s sample field names. Replaces the formernowtest/now_test_oauth/now_auth_info/nowtestauth/nowtest_auth_input.
Architecture
MCP Client (Claude)
↓ stdio / sse
tools.py (FastMCP — 25 tools)
↓
generic_tool_wrappers.py consolidated_tools.py vtb_task_tools.py
cmdb_tools.py utility_tools.py intelligent_query_tools.py
↓
generic_table_tools.py (core query engine, pagination, deterministic sort)
↓
filter/ (v4.0 Sprint 1)
builder.py — ServiceNowQueryBuilder
validator.py — validate_query_filters, validate_and_correct_filters
intelligence.py — QueryIntelligence (NL → filter; no backref to builder)
explainer.py — QueryExplainer
models.py — TableFilterParams, QueryValidationResult
↓
http_layer/ (v4.0 Sprint 3)
url_builder.py — ensure_query_encoded, add_default_params (GET-only)
response_parser.py — extract_display_values (GET-only)
request_dispatcher.py — make_nws_request (~30 lines, dispatches GET vs write)
↓
oauth/ (v4.0 Sprint 3)
token_store.py — token cache + refresh (injectable fetcher)
request_executor.py — authenticated HTTP + 401 retry
client.py — ServiceNowOAuthClient façade
exceptions.py — 4 exception classes
↓
httpx → ServiceNow REST API
GET path applies sysparm_exclude_reference_link=true, sysparm_no_count=true, sysparm_display_value=true, and display-value flattening.
POST/PATCH/DELETE bypass all of the above — enforced by 3 negative tests in tests/test_http_layer.py.
v4.0 shims (deleted in v4.1): query_validation.py, query_intelligence.py, oauth_client.py, service_now_api_oauth.py
Testing
# Full suite
pytest tests/ -v --tb=short
# With coverage
pytest tests/ --cov=. --cov-report=term-missing
575 tests passing, ~83% overall coverage. filter/ 98.16%, oauth/ + http_layer/ 92.98%.
Cloud hosting: Azure Container Apps + Key Vault
For production, store credentials in Azure Key Vault and inject via managed identity. No secrets in env vars, shell history, or docker inspect.
Key Vault (secrets)
↑ reads via RBAC
Container App (managed identity)
↓ injects as env vars via secretRef
mcp-servicenow container
1. Push to Azure Container Registry
az acr create -g -n --sku Basic
az acr login -n
docker tag mcp-servicenow .azurecr.io/mcp-servicenow:latest
docker push .azurecr.io/mcp-servicenow:latest
2. Create Key Vault and store secrets
az keyvault create -g -n --enable-rbac-authorization true
az keyvault secret set --vault-name --name servicenow-instance --value "https://your-instance.service-now.com"
az keyvault secret set --vault-name --name servicenow-client-id --value "your_oauth_client_id"
az keyvault secret set --vault-name --name servicenow-client-secret --value "your_oauth_client_secret"
3. Create Container App with system-assigned managed identity
az containerapp env create -g -n --location westeurope
az containerapp create \
-g -n mcp-servicenow \
--environment \
--image .azurecr.io/mcp-servicenow:latest \
--target-port 8000 --ingress external \
--system-assigned \
--registry-server .azurecr.io
4. Grant Key Vault access to the identity
PRINCIPAL_ID=$(az containerapp show -g -n mcp-servicenow --query identity.principalId -o tsv)
KV_ID=$(az keyvault show -g -n --query id -o tsv)
az role assignment create \
--assignee "$PRINCIPAL_ID" \
--role "Key Vault Secrets User" \
--scope "$KV_ID"
5. Wire Key Vault references into the Container App
az containerapp secret set \
-g -n mcp-servicenow \
--secrets \
"servicenow-instance=keyvaultref:https://.vault.azure.net/secrets/servicenow-instance,identityref:system" \
"servicenow-client-id=keyvaultref:https://.vault.azure.net/secrets/servicenow-client-id,identityref:system" \
"servicenow-client-secret=keyvaultref:https://.vault.azure.net/secrets/servicenow-client-secret,identityref:system"
az containerapp update \
-g -n mcp-servicenow \
--set-env-vars \
"SERVICENOW_INSTANCE=secretref:servicenow-instance" \
"SERVICENOW_CLIENT_ID=secretref:servicenow-client-id" \
"SERVICENOW_CLIENT_SECRET=secretref:servicenow-client-secret"
To rotate a credential: update the Key Vault secret and create a new Container App revision (az containerapp update --revision-suffix vN).
Audit logging
Every tool call emits one structured JSON line to stderr via the AuditMiddleware registered in tools.py. Azure Container Apps automatically ships container stderr to Log Analytics — no extra SDK, no log shipper, no file rotation.
Log line shape (one JSON object per tools/call):
{
"timestamp": "2026-05-25T10:30:00.123Z",
"level": "info",
"event": "tool_call",
"tool": "search_records",
"user": "[email protected]",
"request_id": "req-abc-123",
"args": {"table_name": "incident", "limit": 10},
"duration_ms": 147.32,
"status": "success"
}
Errors add "status": "error", "error": "", and "level": "error".
User identity is parsed from the Authorization: Bearer header (claim priority: preferred_username → upn → email → sub). The JWT signature is NOT verified inside the container — Azure APIM / Front Door / Container Apps ingress must validate the token at the edge before the request reaches the MCP server. Without an Authorization header, user is "unauthenticated". Under stdio transport (local dev), there is no HTTP context and identity tracking is unavailable.
Sensitive arguments matching password, secret, token, key, auth, or credential (case-insensitive substring in the parameter name) are written as "[REDACTED]". Adjust the set in audit_middleware.py.
Query audit logs in Azure
In the Log Analytics workspace attached to the Container Apps environment:
ContainerAppConsoleLogs_CL
| where ContainerAppName_s == "mcp-servicenow"
| where Log_s contains '"event":"tool_call"'
| extend payload = parse_json(Log_s)
| project TimeGenerated, user=payload.user, tool=payload.tool,
status=payload.status, duration_ms=payload.duration_ms,
request_id=payload.request_id, args=payload.args, error=payload.error
| order by TimeGenerated desc
For long-term retention or compliance reporting, route the table to a dedicated Log Analytics workspace with a multi-year retention policy, or export to an immutable Storage account via a Diagnostic Setting.
Dockerfile note: ensure PYTHONUNBUFFERED=1 so log lines flush to stderr immediately and are not buffered across container restarts.
Transport modes
MCP_TRANSPORT |
How it runs | Use case |
|---|---|---|
stdio (default) |
subprocess via stdin/stdout | local Claude Code |
sse |
HTTP server | Docker, cloud, N8N, any network agent |
Override host/port:
docker run -d \
-p 9000:9000 \
-e MCP_TRANSPORT=sse \
-e MCP_HOST=0.0.0.0 \
-e MCP_PORT=9000 \
--env-file .env.local \
mcp-servicenow
N8N: MCP Client node → SSE URL.
LangChain / custom agents: any MCP-compatible SSE client library.
Dependencies
Locked: uv.lock — the exact resolved set; the .mcpb bundle runs uv run --frozen --no-dev against it.
Production floors: requirements.txt (mirrored in pyproject.toml) — only the six packages the code imports; transitive pins live in the lock.
Dev (pytest, coverage, tiktoken, pytest-asyncio): requirements-dev.txt / [dependency-groups] dev in pyproject.toml.
Dev dependencies are never installed in the Docker image.
Documentation
- OAUTH_SETUP_GUIDE.md — ServiceNow OAuth 2.0 setup
- MIGRATION_v3_to_v4.md — v3 → v4 migration guide
- CHANGELOG.md — full change history
- docs/MCPB_BUILD.md — Claude Desktop Extension (
.mcpb) build & release
License
MIT — see LICENSE.
Found a bug? Open an issue.
Install
docker run -d \Configuration
{
"mcpServers": {
"servicenow": {
"command": "python",
"args": ["/full/path/to/personal-mcp-servicenow/personal_mcp_servicenow_main.py"]
}
}
}