servicenow-knowledge-mcp
by Xingyuj
README.md
<div align="center">
# snow-mcp
   
**Read-only enterprise knowledge retrieval over MCP**
ServiceNow Knowledge · Streamable HTTP · Python · Docker
</div>
---
## Overview
A Python 3.15+ / FastMCP 4.x server exposing four read-only ServiceNow Knowledge retrieval tools over stateless Streamable HTTP. It returns source records, not generated answers; it does not provide generic table access, mutations, semantic reranking, OCR, or document parsing. ServiceNow controls downstream access and available content; publication and validity metadata are returned, not an additional MCP publication policy.
## Quick start
From the repository root, with Python 3.15+ and `uv` 0.13.1 or later installed:
```bash
uv python install
uv sync --frozen
cp .env.example .env
```
Set `SERVICENOW_BASE_URL` and either a static `SERVICENOW_ACCESS_TOKEN` or both `SERVICENOW_CLIENT_ID` and `SERVICENOW_CLIENT_SECRET`. For local development keep `MCP_SCOPE_VERIFY_ENABLED=false` and `SERVICENOW_OBO_ENABLED=false`, then run:
```bash
uv run snow-mcp
```
Connect to `http://127.0.0.1:8080/mcp`. The default listener is `0.0.0.0:8080`; set `HOST=127.0.0.1` to restrict local access. Never commit `.env` or real secrets.
| Health endpoint | Meaning |
| --- | --- |
| `GET /mcp/live` | Process can serve HTTP; returns `{"status":"healthy","service":"snow-mcp"}` |
| `GET /mcp/health`, `GET /mcp/ready` | Lifespan is ready; returns `{"status":"ready","service":"snow-mcp"}`, otherwise HTTP 503 |
Readiness is a lifecycle check, not a ServiceNow connectivity or credential check.
The repository pins Python 3.15.0 in `.python-version`; the Docker image uses the same release.
## Docker
```bash
docker build -t snow-mcp .
docker run --rm --env-file .env -e HOST=0.0.0.0 -p 127.0.0.1:8080:8080 snow-mcp
```
The image builds native dependencies in a separate stage with C build tools; the runtime image contains no build toolchain. Initial builds can take longer while Python 3.15 wheels become available.
Use the same ServiceNow configuration as the local setup. The host binding limits access to localhost. These local modes do not authenticate incoming MCP clients; keep them isolated.
## Deployment options
- **Local Python or Docker:** no Azure account is required for integration-token or ServiceNow OAuth client-credential access.
- **Kubernetes:** use the [generic Helm chart](deploy/helm/README.md). Azure identity and Istio are disabled by default.
- **Azure:** [optional deployment examples](examples/deployment/azure/README.md) cover Azure DevOps, AKS, Terraform, and APIM. Replace all example infrastructure values before use.
For network deployments, provide a trusted authentication gateway and prevent direct backend access. The server's claim decoder does not validate JWT signatures. Scope checks require gateway-validated tokens. Entra ID is specifically required for the optional OBO path; Application Insights is optional telemetry. A subscription key is specific to the APIM example, not the MCP protocol.
## Tools and client access
| Tool | Purpose | Required scope when `MCP_SCOPE_VERIFY_ENABLED=true` |
| --- | --- | --- |
| `search_knowledge` | Ordered candidates and snippets; optional limit, knowledge base, language and category | `knowledge.search` |
| `list_kb_categories` | Accessible categories with parent IDs and hierarchy paths | `knowledge.category.read` |
| `get_kb_article` | Canonical content, metadata and attachment references; accepts a 32-hex sys_id or `KB` + 7 digits | `knowledge.article.read` |
| `get_kb_article_attachment` | Size-limited base64 binary content using known article and attachment sys_ids; no parsing | `knowledge.attachment.read` |
Search first unless an article ID is already known, then retrieve the article and relevant attachments. Category listing is for discovery, not article retrieval. Search preserves ServiceNow ordering; it does not promise UI-equivalent or semantic ranking.
### MCP scopes
The service defines four read-only scopes:
- `knowledge.search`: search articles and read result snippets.
- `knowledge.category.read`: list knowledge categories and their hierarchy.
- `knowledge.article.read`: retrieve an article's full content, metadata and attachment references.
- `knowledge.attachment.read`: retrieve an article attachment's binary content.
With `MCP_SCOPE_VERIFY_ENABLED=true`, each tool requires its corresponding scope in the caller's token. Scopes do not imply one another: search access does not grant article or attachment access. With the switch off, tool scope checks are skipped, independently of OBO. These scopes authorize MCP tool calls, not access to individual ServiceNow records; ServiceNow still applies its own permissions to the downstream identity.
For isolated local use, a typical client configuration is:
```json
{
"mcpServers": {
"snow-mcp": {
"type": "http",
"url": "http://127.0.0.1:8080/mcp"
}
}
}
```
Client syntax varies. For remote access, configure the credentials required by your gateway using the client's supported mechanism; do not commit keys or tokens. The [Azure example](examples/deployment/azure/README.md) explains APIM subscription keys and Entra tokens.
## Architecture: two implemented auth paths
`SERVICENOW_OBO_ENABLED` selects the downstream authenticator at runtime
```mermaid
graph TD
Client["MCP Client"] -->|User token| Gateway["Trusted Gateway: JWT Validation"]
Gateway -->|Validated token| MCP["FastMCP Server"]
MCP -->|Select downstream auth| Switch{"OBO Enabled?"}
Switch -->|false| Integration["Integration Authentication"]
Switch -->|true| Entra["Entra OBO Exchange"]
Integration -->|Integration identity| SN["ServiceNow Knowledge APIs"]
Entra -->|Delegated user identity| SN
```
The diagram shows a gateway-backed deployment; isolated local development may connect directly to FastMCP. The gateway performs JWT validation; Azure APIM is one optional implementation. FastMCP extracts claims when OBO or MCP scope verification is enabled; per-tool scope enforcement is controlled independently by `MCP_SCOPE_VERIFY_ENABLED`. The decision node corresponds to `SERVICENOW_OBO_ENABLED`; arrow labels distinguish the identity used for downstream access. Credential selection and prerequisites are detailed below.
| Runtime path | Credentials and identity | Behavior |
| --- | --- | --- |
| **OBO disabled** (`false`, default) | Static ServiceNow token, otherwise ServiceNow OAuth client credentials | ServiceNow authorizes the integration identity, not the caller. A configured static token takes precedence over client credentials. Inbound APIM scope checks remain independently configurable. |
| **OBO enabled** (`true`) | Incoming user assertion + Entra tenant/client/secret + downstream scope | Exchanges the assertion at the tenant-specific Entra v2 token endpoint, then calls ServiceNow with the delegated token. Independent of `MCP_SCOPE_VERIFY_ENABLED`; integration-token/client-credential settings are not used and there is no fallback to them. |
OBO caches tokens per incoming assertion, bounded by both token lifetimes (maximum 256 cache entries). ServiceNow must already accept the downstream Entra token and map the user to the intended ACLs. App registration, delegated permissions/consent and ServiceNow trust/user mapping are deployment prerequisites, not provisioned by this server.
### Security boundary
The authentication gateway must validate the incoming JWT and forward its original `Authorization` header. With OBO enabled, ClientApp may instead forward that validated token as `x-mcp-servicenow-auth: Bearer <token>`. This header is used only when `Authorization` is absent, before claim extraction and OBO context creation. An empty or invalid `Authorization` header does not trigger fallback. The gateway must validate the token in whichever header is used and strip untrusted caller-supplied forwarding headers. The MCP verifier decodes trusted claims and applies tool scopes; **it does not validate JWT signatures, issuer or audience**. `APIM_EXPECTED_*` values only produce diagnostic comparisons, never enforce token validation. Block direct backend access and ensure **every** permitted route has the same validation guarantee before enabling APIM auth. With both scope verification and OBO disabled, MCP does not enforce inbound authentication or tool scopes; use this only for isolated local development or an explicitly secured deployment boundary.
TLS uses the operating-system trust store. The Docker image uses the system CA bundle; no organization certificate is bundled. Logs exclude raw credentials, authorization headers and article bodies; DEBUG diagnostics can include token fingerprints and claim comparisons, so restrict log access and retention. Application Insights is optional and initialization failures do not prevent startup.
## Environment variable configuration
The application reads settings from `.env` in the working directory and from the process environment; process values take precedence. Names are case-insensitive. Unknown `.env` keys are ignored. Settings are cached: restart after changes. Defaults below are the **code defaults**, not example-file placeholders or Helm overrides. `unset` means no configured value; use omission for optional settings unless blank behavior is explicitly stated. Secrets belong in an approved secret manager/Kubernetes Secret, not a ConfigMap.
The tables cover every application setting and deployment-related variables. Recommendations are examples, not existing prod deployment configuration. `dev` means isolated local development; deployed dev should use the same APIM boundary as test/prod.
**Required / failure impact** distinguishes values you must supply from settings whose defaults already work. **Conditional** requirements apply only to the selected feature or authentication path; optional does not mean an invalid supplied value is accepted. Invalid types, ranges or constrained values can fail settings validation at startup. OBO/APIM configuration checks run during server construction. ServiceNow URL, integration credentials and the default/max search-limit relationship are checked when the retrieval service is first initialized (normally the first tool call), not by the readiness endpoint. Missing retrieval credentials can therefore leave HTTP/health running while tools fail. Valid configuration alone does not guarantee valid credentials, upstream connectivity, permissions or compatible API routes.
### Connection and integration authentication
| Variable | Required / failure impact | Code default | Purpose / affected functionality | Dev / test / prod recommendation or example |
| ---| --- | --- | --- | --- |
| `SERVICENOW_BASE_URL` | **Required** for all retrieval; no usable default | `""` (required) | ServiceNow origin for all retrieval and integration OAuth calls; trailing `/` removed | Separate instances: `https://dev-instance.service-now.com` / `https://test-instance.service-now.com` / `https://prod-instance.service-now.com` |
| `SERVICENOW_KNOWLEDGE_API_PATH` | Default sufficient; correct route required for search, articles and attachments | `api/sn_km_api/knowledge/articles` | Search, article and article attachment API route | Keep default in all environments unless instance routing differs |
| `SERVICENOW_CATEGORIES_API_PATH` | Default sufficient; correct route required for category listing | `api/now/table/kb_category` | Category hierarchy API route | Keep default in all environments |
| `SERVICENOW_ACCESS_TOKEN` | **Conditional**: OBO off; supply this **or** both integration client credentials | unset | Static outbound bearer token when OBO is off; takes precedence over integration client credentials | Dev: optional short-lived token; test/prod: omit when using managed client credentials or OBO |
| `SERVICENOW_CLIENT_ID` | **Conditional**: OBO off and no static token; required with client secret | unset | Integration OAuth client ID when OBO is off and no static token is configured | Dedicated least-privilege client per instance; unused with OBO |
| `SERVICENOW_CLIENT_SECRET` | **Conditional**: OBO off and no static token; required with client ID | unset | Secret paired with integration client ID | Dev: local secret; test/prod: secret-manager injection and rotation; unused with OBO |
| `SERVICENOW_OAUTH_TOKEN_PATH` | Default sufficient; correct route required for integration client credentials | `oauth_token.do` | Integration client-credentials token endpoint | Default in all environments unless instance requires another route |
| `SERVICENOW_OAUTH_SCOPE` | Optional; required only if the integration OAuth provider requires a scope | unset | Optional scope sent in integration token request | Omit unless required by the instance; use that instance's approved scope in each environment |
| `SERVICENOW_API_VERSION` | Optional; required only if the ServiceNow instance requires a version header | unset | Optional `Accept-Version` header for ServiceNow requests | Omit unless the instance requires a specific version; test it before prod |
### APIM and delegated OBO authentication
`MCP_SCOPE_VERIFY_ENABLED` controls tool scope checks independently of OBO. Trusted JWT claim extraction runs when either switch is enabled; OBO always requires a user token, even when tool scope checks are disabled. The gateway remains responsible for cryptographic JWT validation.
| Variable | Required / failure impact | Code default | Purpose / affected functionality | Dev / test / prod recommendation or example |
| ---| --- | --- | --- | --- |
| `MCP_SCOPE_VERIFY_ENABLED` | Optional; independent of OBO | `false` | Enables fixed per-tool scope checks; OBO still extracts user claims when this is off | Enable only with validated-token forwarding and blocked bypass routes |
| `SERVICENOW_OBO_ENABLED` | Optional; must be `true` to use delegated user access (default uses integration identity) | `false` | Selects delegated OBO instead of integration authentication | Dev: `false` for integration setup; test/prod: `true` if user-level ACLs and downstream trust are configured, otherwise `false` with intentional integration ACLs |
| `ENTRA_TENANT_ID` | **Conditional**: required when OBO is enabled | unset | Tenant-specific OBO endpoint; required with OBO; `common`, `organizations`, `consumers` rejected | Actual tenant GUID per environment; omit without OBO |
| `ENTRA_CLIENT_ID` | **Conditional**: required when OBO is enabled | unset | Middle-tier Entra app performing OBO; required with OBO | Environment-specific app registration ID; omit without OBO |
| `ENTRA_CLIENT_SECRET` | **Conditional**: required when OBO is enabled | unset | Middle-tier app secret; required with OBO | Dev: local secret; test/prod: secret-manager injection and rotation; omit without OBO |
| `SERVICENOW_OBO_SCOPE` | **Conditional**: required when OBO is enabled | unset | Downstream delegated scope requested from Entra; required with OBO | Example `api://<downstream-app-id>/.default`, only if that resource has approved delegated permissions/consent; use environment-specific resource |
| `APIM_SCOPE_CLAIM_NAMES` | Default sufficient; non-empty matching claims required when APIM auth is enabled | `scp,scope,roles` | Comma-separated claims merged for tool authorization | Keep default in all environments, or limit to the claims actually validated/issued by your identity setup; must be non-empty with APIM auth |
| `APIM_SUBJECT_CLAIM_NAMES` | Default sufficient; non-empty matching claims required when APIM auth is enabled | `oid,sub` | Ordered claims used to identify the caller | Keep default in all environments; must be non-empty with APIM auth |
| `APIM_EXPECTED_AUDIENCE` | Optional; no explicit value required | `""` | Diagnostic audience comparison only | Local: blank; test/prod: expected middle-tier API audience, e.g. `api://<mcp-app-id>`; enforce separately in APIM |
| `APIM_EXPECTED_ISSUER` | Optional; no explicit value required | `""` | Diagnostic issuer comparison only | Local: blank; test/prod: exact issuer configured in APIM, e.g. `https://login.microsoftonline.com/<tenant-id>/v2.0` |
| `APIM_EXPECTED_TENANT_ID` | Optional; no explicit value required | `""` | Diagnostic tenant comparison only | Local: blank; test/prod: expected tenant GUID; enforce separately in APIM |
| `TOKEN_FINGERPRINT_KEY` | Optional; if supplied, must be exactly 64 hex characters or settings validation fails | unset | DEBUG token correlation: unset/blank uses SHA-256; a key uses truncated HMAC-SHA-256 | Dev: omit unless troubleshooting; test/prod: separate secret of exactly 64 hexadecimal characters if diagnostics are needed; never reuse an app secret |
Tool scopes are fixed in `src/snow_mcp/scopes.py`, not configurable by environment variables. OBO requires all four `ENTRA_*`/scope values above and a gateway-validated user token, but does not require tool scope verification to be enabled. Missing or invalid OBO settings fail configuration validation.
### Retrieval filters and fields
| Variable | Required / failure impact | Code default | Purpose / affected functionality | Dev / test / prod recommendation or example |
| ---| --- | --- | --- | --- |
| `SERVICENOW_KNOWLEDGE_BASE` | Optional; no explicit value required | unset | Default knowledge-base identifier for search; a tool argument can override it | Dev: omit or sandbox sys_id; test/prod: intended base sys_id if a default is needed; not an authorization boundary |
| `SERVICENOW_LANGUAGE` | Optional; no explicit value required | unset | Default search language; a tool argument can override it | Omit for unrestricted language; use e.g. `en` in all environments if required |
| `SERVICENOW_SEARCH_CATEGORY_ALLOWLIST` | Optional | `""` | Search-only category display-name restriction; unset or blank disables it | Set approved comma-separated category names when restrictions are required |
| `SERVICENOW_SEARCH_KNOWLEDGE_BASE_ALLOWLIST` | Optional; blank leaves knowledge bases unrestricted by this filter | `""` | Search-only knowledge-base display-name restriction; blank disables it | Dev: blank or sandbox base; test/prod: explicitly approved names, e.g. `HR,IT` |
| `SERVICENOW_SEARCH_FIELDS` | Default sufficient; retain category/base fields when their search filters are active | `sys_id,number,short_description,description,kb_knowledge_base,kb_category,score` | Fields requested for search result mapping and allowlist checks | Keep default in all environments, especially `kb_category` and `kb_knowledge_base` |
| `SERVICENOW_ARTICLE_FIELDS` | Default sufficient; retain content/metadata fields for complete article retrieval | `sys_id,number,short_description,text,kb_knowledge_base,kb_category,workflow_state,published,valid_to,sys_updated_on` | Fields requested for canonical article content and metadata | Keep default in all environments; removing `text` or metadata reduces returned content |
| `SERVICENOW_CATEGORY_FIELDS` | Default sufficient; retain ID/name/parent fields for category hierarchy | `sys_id,label,value,parent_id,full_category,active` | Fields requested for category IDs, names and hierarchy | Keep default in all environments |
Allowlists use comma-separated display names, trimmed and matched case-insensitively. Names in each list are ORed; the two lists are ANDed. Search sends an encoded-query `filter` and also checks returned candidates locally. Missing/nonmatching fields are excluded when their list is non-empty; ranks are preserved, `total` counts retained results, and no extra request fills filtered-out slots. A caller's `category` narrows, never overrides, the category allowlist. Names containing `^`, line breaks or a leading `javascript:` are rejected. These filters do **not** restrict category listing, direct article retrieval or attachment retrieval; downstream ACLs remain the access boundary.
### Bounds, reliability and observability
| Variable | Required / failure impact | Code default | Purpose / affected functionality | Dev / test / prod recommendation or example |
| ---| --- | --- | --- | --- |
| `REQUEST_TIMEOUT_SECONDS` | Default sufficient; no explicit value required | `10` | HTTP timeout for ServiceNow and token exchanges; range `(0,120]` | Start with `10` in all environments; raise only after measuring upstream latency |
| `DEFAULT_SEARCH_LIMIT` | Default sufficient; no explicit value required | `5` | Result limit when caller omits it; at least 1 and no greater than max | `5` in all environments |
| `MAX_SEARCH_LIMIT` | Default sufficient; no explicit value required | `20` | Upper bound for search results; at least 1 | `20` in all environments; reduce if response budgets require it |
| `CATEGORY_PAGE_SIZE` | Default sufficient; no explicit value required | `100` | Category pagination batch size; range 1–500 | `100` in all environments |
| `MAX_ARTICLE_CONTENT_CHARS` | Default sufficient; no explicit value required | `100000` | Article text truncation threshold; minimum 100 | `100000` in all environments; lower if client context budgets require it |
| `MAX_ATTACHMENT_BYTES` | Default sufficient; no explicit value required | `5000000` | Maximum attachment download size; minimum 1; oversized downloads rejected | `5000000` in all environments; consider base64 response overhead before increasing |
| `TRANSIENT_RETRY_ATTEMPTS` | Default sufficient; no explicit value required | `2` | Extra retries for transient ServiceNow retrieval failures; range 0–5, not an OBO exchange retry setting | `2` in all environments; `0` for deterministic failure testing |
| `RETRY_BACKOFF_SECONDS` | Default sufficient; no explicit value required | `0.2` | Base delay for retrieval retry backoff; range 0–10 | `0.2` in all environments; tune against upstream rate limits |
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | **Conditional**: required only for Application Insights telemetry; server works without it | unset | Enables optional Azure Monitor telemetry; missing/placeholder disables it | Dev: omit or dedicated dev resource; test/prod: inject respective resource connection string |
| `ENVIRONMENT` | Default sufficient; no explicit value required | `development` | Environment label in lifecycle/telemetry setup logs; does not switch auth or retrieval behavior | `dev` / `test` / `prod` |
| `LOG_LEVEL` | Default sufficient; no explicit value required | `INFO` | Application logging verbosity, including DEBUG auth diagnostics | Dev: `DEBUG` temporarily; test/prod: `INFO`, DEBUG only during controlled troubleshooting |
| `HOST` | Default sufficient; no explicit value required | `0.0.0.0` | CLI server bind address | Local dev: `127.0.0.1`; container test/prod: `0.0.0.0` behind a protected ingress |
| `PORT` | Default sufficient; no explicit value required | `8080` | CLI server port; range 1–65535 | `8080` in all environments unless platform requires another port |
### Deployment variables
These are **not** application settings in `ServiceNowKnowledgeConfig`:
| Variable | Required / failure impact | Default | Consumer / effect | Dev / test / prod recommendation or example |
| ---| --- | --- | --- | --- |
| `PATH_BASE` | Not required by Python server; deployment routing is configured separately | No application default | Not injected by the generic chart; Python server does not read it or change `/mcp` with it | Set routing through Helm/Istio/APIM, not this variable alone; e.g. environment suffix `dev` / `test` / `prod` |
Helm runs Uvicorn with `mcp.args.host` and `mcp.args.port` as explicit command arguments; those, not `HOST`/`PORT`, control the Helm listener. ConfigMap values come from `config`, and `secretEnv.existingSecret` supplies secret environment values. `.env.example` is for local setup, not a production manifest.
The Dockerfile also sets these runtime/library variables (not Pydantic settings). Keep the image defaults across dev/test/prod unless your platform explicitly requires an override:
| Variable | Required / failure impact | Docker default | Purpose / affected functionality |
| ---| --- | --- | --- |
| `PYTHONDONTWRITEBYTECODE` | Default sufficient; no explicit value required | `1` | Prevents Python from writing `.pyc` files |
| `PYTHONUNBUFFERED` | Default sufficient; no explicit value required | `1` | Flushes Python output promptly for container log collection |
| `FASTMCP_CHECK_FOR_UPDATES` | Default sufficient; no explicit value required | `off` | Disables FastMCP update checks |
| `PATH` | Default sufficient; no explicit value required | `/app/.venv/bin:$PATH` | Resolves commands from the image's Python virtual environment |
| `SSL_CERT_FILE` | Default sufficient; no explicit value required | `/etc/ssl/certs/ca-certificates.crt` | CA bundle for compatible TLS libraries; application HTTP clients explicitly use the OS trust store |
| `REQUESTS_CA_BUNDLE` | Default sufficient; no explicit value required | `/etc/ssl/certs/ca-certificates.crt` | CA bundle for Requests-based dependencies; not a switch to disable TLS verification |
Docker builds install locked dependencies from the public package index and use the system CA bundle. Configure any private package feed or organization CA outside this repository.
### Example environment profiles
Shared safe starting values (apply the correct instance and explicit search allowlists from the tables for **each** environment):
```dotenv
REQUEST_TIMEOUT_SECONDS=10
DEFAULT_SEARCH_LIMIT=5
MAX_SEARCH_LIMIT=20
MAX_ATTACHMENT_BYTES=5000000
LOG_LEVEL=INFO
PORT=8080
```
Isolated local **dev**, integration identity:
```dotenv
ENVIRONMENT=dev
HOST=127.0.0.1
MCP_SCOPE_VERIFY_ENABLED=false
SERVICENOW_OBO_ENABLED=false
SERVICENOW_BASE_URL=https://dev-instance.service-now.com
SERVICENOW_CLIENT_ID=<dev-integration-client-id>
SERVICENOW_CLIENT_SECRET=<inject-dev-secret>
```
APIM-backed **test**, exercising delegated OBO:
```dotenv
ENVIRONMENT=test
HOST=0.0.0.0
MCP_SCOPE_VERIFY_ENABLED=true
SERVICENOW_OBO_ENABLED=true
SERVICENOW_BASE_URL=https://test-instance.service-now.com
ENTRA_TENANT_ID=<test-tenant-guid>
ENTRA_CLIENT_ID=<test-middle-tier-app-id>
ENTRA_CLIENT_SECRET=<inject-test-secret>
SERVICENOW_OBO_SCOPE=api://<test-downstream-app-id>/.default
```
APIM-backed **prod** uses the tested identity path. For delegated access use the test profile with production instance, tenant, app, scope. Set `ENVIRONMENT=prod` and inject credentials separately, and the production Application Insights connection string. For intentionally shared integration access use this profile instead:
```dotenv
ENVIRONMENT=prod
HOST=0.0.0.0
MCP_SCOPE_VERIFY_ENABLED=true
SERVICENOW_OBO_ENABLED=false
SERVICENOW_BASE_URL=https://prod-instance.service-now.com
SERVICENOW_CLIENT_ID=<prod-integration-client-id>
SERVICENOW_CLIENT_SECRET=<inject-prod-secret>
APPLICATIONINSIGHTS_CONNECTION_STRING=<prod-resource-connection-string>
```
Placeholders are illustrative, not usable credentials. Omit `SERVICENOW_ACCESS_TOKEN` when using client credentials. Neither enabling OBO nor APIM auth configures the external identity provider, gateway policy or ServiceNow ACLs automatically.
## Development
```bash
uv sync --frozen --extra dev
uv run pytest
uv run ruff format --check src tests
uv run ruff check src tests
uv run mypy src
```
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues