# Snow Meta Tool

> ServiceNow Meta Tool (MCP Server) exposing ServiceNow as 3 generic tools (discover, query, write) with per-user identity propagation via Azure APIM JWT Bearer OBO flow

- **Type:** MCP server
- **Install:** `agentstack add mcp-ozgurkarahan-snow-meta-tool`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ozgurkarahan](https://agentstack.voostack.com/s/ozgurkarahan)
- **Installs:** 0
- **Category:** [Cloud & Infrastructure](https://agentstack.voostack.com/c/cloud-infrastructure)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ozgurkarahan](https://github.com/ozgurkarahan)
- **Source:** https://github.com/ozgurkarahan/snow-meta-tool

## Install

```sh
agentstack add mcp-ozgurkarahan-snow-meta-tool
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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

> [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://.service-now.com
export SN_CLIENT_ID=
export SN_JWT_KID=
export SN_JWT_KEY_PATH=../../certs/sn-jwt-bearer.key
export SN_JWT_SUB=

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://.service-now.com \
  -e SN_CLIENT_ID= \
  -e SN_JWT_KID= \
  -e SN_JWT_KEY_PATH=/app/certs/key.pem \
  -e SN_JWT_SUB= \
  -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://.service-now.com"
azd env set SN_OAUTH_CLIENT_ID ""
azd env set SN_JWT_BEARER_KID ""
azd env set AZURE_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://.service-now.com \
>   --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

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ozgurkarahan](https://github.com/ozgurkarahan)
- **Source:** [ozgurkarahan/snow-meta-tool](https://github.com/ozgurkarahan/snow-meta-tool)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-ozgurkarahan-snow-meta-tool
- Seller: https://agentstack.voostack.com/s/ozgurkarahan
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
