MCP server exposing ServiceNow as 3 generic tools (discover, query, write) with per-user identity propagation. An AI agent discovers tables and fields at runtime, then queries and writes records as the authenticated user — no service account, no shared credentials.
Part of the Identity-Propagated MCP Meta-Tool series — building enterprise-grade MCP tools where every action is traceable to the real user, not a service account. Each tool follows the same pattern: generic tools + runtime schema discovery + On-Behalf-Of identity flow through Azure APIM.
Tool System Status Salesforce Meta-Tool Salesforce CRM ✅ Production ServiceNow Meta-Tool (this repo) ServiceNow ITSM ✅ Production When both tools are connected to the same agent, it can work across CRM and ITSM in a single conversation — see joint use cases.
Most MCP integrations build one tool per API endpoint — list_incidents, create_change_request, get_user, etc. This creates a combinatorial explosion: ServiceNow has 4,000+ tables, each with dozens of fields. You'd need thousands of tools, and the agent would struggle to pick the right one.
The meta-tool pattern flips this: 3 generic tools cover the entire ServiceNow data model. The agent discovers the schema at runtime and constructs queries dynamically:
| ❌ Traditional Approach | ✅ Meta-Tool Approach |
|---|---|
list_incidents(priority, state) |
query(table="incident", query="priority=1^state=2") |
create_change_request(title, ...) |
write(table="change_request", operation="create", ...) |
get_incident_fields() |
discover(table="incident") |
| 100+ hardcoded tools | 3 tools for any table |
🧠 This works because LLMs are good at composing queries from natural language — they don't need pre-built tool signatures for every table.
Most AI integrations use a shared service account — every user's actions appear as "API Bot" in the audit log. This creates three problems:
| Problem | Impact | |
|---|---|---|
| 👤 | No accountability | Can't tell who did what — audit trail shows the service account, not the user |
| 🔓 | Over-privileged access | Service account needs permissions for everything any user might do |
| ⚖️ | Compliance risk | SOX, HIPAA, ITIL require actions traceable to individual users |
This project solves all three with On-Behalf-Of (OBO) identity propagation:
User "alice@company.com" asks agent: "Create a P2 incident for the network outage"
❌ Traditional: ServiceNow sees → sys_created_by: "api_service_account"
✅ This project: ServiceNow sees → sys_created_by: "alice@company.com"
🛡️ The APIM policy exchanges the user's Azure AD token for a ServiceNow token scoped to that specific user. Every API call runs with that user's permissions and appears in the audit log under their name.
Azure AD
│
│ Bearer token (user identity)
▼
┌───────────────┐
│ Azure APIM │
│ OBO Policy │
└───────┬───────┘
│
1. Validate Azure AD token
2. Extract user email (preferred_username / upn)
3. Build JWT Bearer assertion (RS256, per-user sub)
4. Exchange at ServiceNow oauth_token.do
5. Cache SN token per user (25 min)
6. Forward request with SN Bearer token
│
▼
┌────────────────────────┐
│ servicenow-mcp │
│ (Container App) │
│ │
│ FastMCP + 3 tools: │
│ 🔍 discover │
│ 📊 query │
│ ✏️ write │
└────────┬───────────────┘
│
│ ServiceNow Table API
│ (Bearer token per user)
▼
┌──────────────────┐
│ ServiceNow │
│ Instance │
└──────────────────┘
🔄 Identity flow: Azure AD user → APIM extracts email from JWT → builds ServiceNow JWT Bearer assertion with sub=email → exchanges for ServiceNow access token → all API calls run as that ServiceNow user.
Two auth modes:
- 🌐 Passthrough (APIM): Bearer token injected by Azure API Management. The APIM policy handles the Azure AD → ServiceNow token exchange. No secrets on the MCP server.
- 💻 Self-managed (local dev): JWT Bearer exchange at
oauth_token.dousing a local RSA private key. Useful for development and testing without APIM.
Two modes:
- Table search:
discover(filter="incident")— searchsys_db_objectby name/label - Field metadata:
discover(table="incident")— querysys_dictionaryfor field definitions - + Picklists:
discover(table="incident", include_choices=True)— adds choice values fromsys_choice
Three modes:
- Record query:
query(table="incident", query="priority=1^state!=6", fields="number,short_description")— encoded query with auto-pagination - Text search:
query(table="incident", text_search="password reset")— full-text search via TEXTQUERY operator - Aggregate:
query(table="incident", aggregate=True, group_by="priority")— counts/sums/averages via Stats API
Three operations:
- Create:
write(table="incident", operation="create", field_values={...}) - Update:
write(table="incident", operation="update", sys_id="...", field_values={...}) - Delete:
write(table="incident", operation="delete", sys_id="...")
📋 Approvals are just table writes: update
sysapproval_approverwithstate=approved/rejected.
- 🐍 Python 3.12+
- 🔑 RSA key pair for JWT signing
- ☁️ ServiceNow instance with OAuth JWT Bearer app configured (setup guide)
cd src/servicenow-mcp
pip install -r requirements.txt
export SN_INSTANCE_URL=https://<instance>.service-now.com
export SN_CLIENT_ID=<oauth_client_id>
export SN_JWT_KID=<kid_from_jwt_verifier_map>
export SN_JWT_KEY_PATH=../../certs/sn-jwt-bearer.key
export SN_JWT_SUB=<user_email>
python app.py
# 🟢 Server starts on http://localhost:8000
# Health check: GET /health
# MCP endpoint: POST /mcpcd src/servicenow-mcp
docker build -t servicenow-mcp .
docker run -p 8000:8000 \
-e SN_INSTANCE_URL=https://<instance>.service-now.com \
-e SN_CLIENT_ID=<client_id> \
-e SN_JWT_KID=<kid> \
-e SN_JWT_KEY_PATH=/app/certs/key.pem \
-e SN_JWT_SUB=<user_email> \
-v ./certs:/app/certs:ro \
servicenow-mcp# Generate PFX from existing key + cert
openssl pkcs12 -export -out certs/sn-jwt-bearer.pfx \
-inkey certs/sn-jwt-bearer.key -in certs/sn-jwt-bearer.crt -passout pass:
# Initialize and configure
azd init
azd env set SN_INSTANCE_URL "https://<instance>.service-now.com"
azd env set SN_OAUTH_CLIENT_ID "<client_id>"
azd env set SN_JWT_BEARER_KID "<kid>"
azd env set AZURE_RESOURCE_GROUP "<existing-resource-group>"
# Set shared resource names (APIM, ACR, KV, CAE, etc.)
# 🚀 Provision infrastructure + deploy
azd upThe
azd provisionstep creates: Container App, APIM API + backend + OBO policy, Named Values, Key Vault cert, and AI Foundry connection. The post-provision hook uploads the PFX certificate to Key Vault and configures the APIM certificate binding.
| Variable | Required | Default | Purpose |
|---|---|---|---|
SN_INSTANCE_URL |
✅ Yes | — | ServiceNow instance URL |
SN_CLIENT_ID |
💻 Self-managed | — | OAuth JWT Bearer client_id |
SN_JWT_KID |
💻 Self-managed | — | kid from jwt_verifier_map |
SN_JWT_KEY_PATH |
💻 Self-managed | — | Path to RSA private key (.pem) |
SN_JWT_SUB |
💻 Self-managed | — | JWT sub claim (user email) |
PORT |
❌ No | 8000 |
HTTP listen port |
APPLICATIONINSIGHTS_CONNECTION_STRING |
❌ No | — | Azure Monitor telemetry |
In passthrough mode (behind APIM), only
SN_INSTANCE_URLis required. The bearer token comes from the APIM policy via theAuthorizationheader.
| Path | Description |
|---|---|
📄 src/servicenow-mcp/app.py |
FastMCP server + 3 tools + middleware |
📄 src/servicenow-mcp/servicenow_client.py |
Async ServiceNow REST client (JWT auth, caching) |
🐳 src/servicenow-mcp/Dockerfile |
Multi-stage Docker build |
🏗️ infra/main.bicep |
Root IaC module (resource-group scoped) |
🏗️ infra/modules/ |
Bicep modules: Container App, APIM API, cert, Foundry connection |
📜 infra/policies/ |
APIM XML policies: OBO token exchange, PRM metadata |
🔧 hooks/postprovision.py |
Post-deploy: cert upload, APIM binding, Named Values, Foundry |
🧪 scripts/test_jwt_bearer.py |
Automated SN instance setup + feasibility test |
🔑 certs/ |
RSA keys for JWT signing (gitignored) |
The project deploys into an existing resource group alongside the Salesforce MCP tool, sharing:
| Resource | Shared With |
|---|---|
| 🌐 Azure API Management (APIM) | SF MCP |
| 📦 Container Apps Environment | SF MCP |
| 🐳 Azure Container Registry (ACR) | SF MCP |
| 🔑 Key Vault | SF MCP |
| 📊 Application Insights | SF MCP |
| 🤖 AI Foundry project | SF MCP |
SN-specific resources created by azd provision:
- 📦 Container App (
ca-sn-mcp) — runs the FastMCP server - 🌐 APIM API (
servicenow-mcp-obo) — native MCP type with OBO policy - 🏷️ APIM Named Values — SN OAuth client ID, instance URL, JWT kid, cert thumbprint
- 🔐 APIM Certificate — JWT Bearer signing cert (from Key Vault)
- 🤖 Foundry Connection (
servicenow-obo) — RemoteTool with UserEntraToken auth
The APIM policy (infra/policies/sn-mcp-obo-policy.xml) handles the full token exchange:
| Step | Action |
|---|---|
| 1️⃣ | Validate Azure AD token (v1 + v2 issuers, audience: https://ai.azure.com) |
| 2️⃣ | Extract user email from preferred_username / upn / unique_name claims |
| 3️⃣ | Build RS256 JWT Bearer assertion with sub=email, kid, unique jti |
| 4️⃣ | Exchange assertion at ServiceNow oauth_token.do for a user-scoped access token |
| 5️⃣ | Cache the ServiceNow token per user for 25 min (5-min safety margin on 30-min lifetime) |
| 6️⃣ | Forward the request with the ServiceNow Bearer token + X-User-Email header |
📡 A separate PRM endpoint (
/.well-known/oauth-protected-resource) advertises Azure AD as the authorization server per RFC 9728.
Required configuration on the ServiceNow instance:
| Step | What | Details |
|---|---|---|
| 1️⃣ | 🔐 X.509 Certificate | Upload to sys_certificate (type: trust_store_cert) |
| 2️⃣ | 🔑 OAuth JWT App | Create in oauth_jwt with inbound_grant_type="jwt", public_client=true, user_field="email" |
| 3️⃣ | 🗺️ JWT Verifier Map | Create in jwt_verifier_map linking kid to certificate |
| 4️⃣ | 👤 User | Non-admin user with itil + personalize_dictionary roles |
🤖 The
scripts/test_jwt_bearer.pyscript automates all of this:python scripts/test_jwt_bearer.py \ --instance https://<instance>.service-now.com \ --admin-password "<admin_password>"
| Role | Purpose | Required For |
|---|---|---|
🟢 itil |
Base ITSM role — read/write incidents, changes, problems | All basic operations |
🟡 personalize_dictionary |
Read sys_dictionary table (field metadata) |
discover(table=...), field validation in write |
🔴 admin |
Full access | BLOCKED — ServiceNow rejects JWT Bearer for admin users |
💡 Recommended: Assign
itil+personalize_dictionaryto MCP users. The server gracefully degrades ifpersonalize_dictionaryis missing.
ServiceNow uses encoded query strings (not SQL). Key operators:
| Operator | Syntax | Example |
|---|---|---|
| Equals | field=value |
priority=1 |
| Not equals | field!=value |
state!=6 |
| Contains | fieldLIKEvalue |
short_descriptionLIKEpassword |
| Starts with | fieldSTARTSWITHvalue |
numberSTARTSWITHINC |
| Greater than | field>value |
priority>2 |
| In list | fieldINvalue1,value2 |
priorityIN1,2,3 |
| Empty | fieldISEMPTY |
assigned_toISEMPTY |
| AND | ^ |
priority=1^state=2 |
| OR | ^OR |
priority=1^ORpriority=2 |
| Order by | ^ORDERBYfield |
^ORDERBYDESCsys_created_on |
| Document | Description |
|---|---|
| 📋 TEST_PROMPTS.md | 10 business scenarios with 40+ agent prompts — service desk triage, change management, major incidents, executive dashboards, and Salesforce CRM + ServiceNow ITSM joint workflows |
| 🎯 DEMO_DATA_SETUP.md | Step-by-step guide to populate both Salesforce and ServiceNow with correlated demo data for cross-platform scenarios |
- salesforce-meta-tool-identity-propagation — The Salesforce counterpart: same meta-tool pattern and OBO identity propagation, applied to Salesforce CRM. Deploy both side-by-side to power a Customer 360 AI Foundry agent that correlates Salesforce CRM data with ServiceNow incidents, changes, and requests under the user's own identity in each system.
MIT
