Content
# ServiceNow MCP Meta-Tool
[](https://modelcontextprotocol.io/)
[](https://developer.servicenow.com/)
[](https://learn.microsoft.com/azure/api-management/)
[](https://www.python.org/)
[](LICENSE)
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.
## Architecture
<img src="docs/diagrams/azure-architecture.png" alt="Azure Architecture — Identity-Propagated MCP Meta-Tools">
> [draw.io source](docs/diagrams/azure-architecture.drawio)
> **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](https://github.com/ozgurkarahan/salesforce-meta-tool-identity-propagation) | 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](TEST_PROMPTS.md#-scenario-10-salesforce-crm--servicenow-itsm-joint-use-cases).
>
> 📖 **Read the story:** [From Theory to Production: Salesforce Meta-Tool & Identity Propagation](https://www.linkedin.com/pulse/from-theory-production-salesforce-meta-tools-identity-ozgur-karahan-ch30e/) — no service accounts in the data path, every action traceable to a real user. Companion repo: [propagate-id-entra](https://github.com/ozgurkarahan/propagate-id-entra) — the end-to-end Entra ID identity-propagation pattern this series builds on.
---
## 💡 Why a Meta-Tool?
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.
---
## 🔐 Why Identity Propagation?
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.
---
## 🏗️ Architecture
```
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.do` using a local RSA private key. Useful for development and testing without APIM.
---
## 🛠️ Tools
### 🔍 `discover` — Table & Field Discovery
Two modes:
- **Table search:** `discover(filter="incident")` — search `sys_db_object` by name/label
- **Field metadata:** `discover(table="incident")` — query `sys_dictionary` for field definitions
- **+ Picklists:** `discover(table="incident", include_choices=True)` — adds choice values from `sys_choice`
### 📊 `query` — Read Records, Search, Aggregate
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
### ✏️ `write` — Create, Update, Delete
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_approver` with `state=approved/rejected`.
---
## 🚀 Quick Start
### 📋 Prerequisites
- 🐍 Python 3.12+
- 🔑 RSA key pair for JWT signing
- ☁️ ServiceNow instance with OAuth JWT Bearer app configured ([setup guide](#-servicenow-instance-setup))
### 💻 Local Development
```bash
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 /mcp
```
### 🐳 Docker
```bash
cd 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
```
### ☁️ Deploy to Azure
```bash
# 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 up
```
> The `azd provision` step 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.
---
## ⚙️ Environment Variables
| 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_URL` is required. The bearer token comes from the APIM policy via the `Authorization` header.
---
## 📁 Key Paths
| 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) |
---
## 🏢 Infrastructure
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
### 🔄 APIM OBO Policy
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.
---
## ☁️ ServiceNow Instance Setup
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.py` script **automates all of this:**
> ```bash
> python scripts/test_jwt_bearer.py \
> --instance https://<instance>.service-now.com \
> --admin-password "<admin_password>"
> ```
### 👥 ServiceNow Roles
| 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_dictionary` to MCP users. The server gracefully degrades if `personalize_dictionary` is missing.
---
## 📖 ServiceNow Encoded Query Syntax
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` |
---
## 🧪 Testing & Demo
| Document | Description |
|---|---|
| 📋 [TEST_PROMPTS.md](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](DEMO_DATA_SETUP.md) | Step-by-step guide to populate both Salesforce and ServiceNow with correlated demo data for cross-platform scenarios |
---
## 🔗 Related Projects
- **[salesforce-meta-tool-identity-propagation](https://github.com/ozgurkarahan/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.
---
## 📄 License
MIT
Connection Info
You Might Also Like
cc-switch
All-in-One Assistant for Claude Code, Codex & Gemini CLI across platforms.
awesome-claude-skills
A curated list of awesome Claude Skills, resources, and tools for...
claude-flow
Claude-Flow v2.7.0 is an enterprise AI orchestration platform.
Appwrite
Build like a team of hundreds
semantic-kernel
Build and deploy intelligent AI agents with Semantic Kernel's orchestration...
Anthropic-Cybersecurity-Skills
734+ structured cybersecurity skills for AI agents · MITRE ATT&CK mapped ·...