Skip to main content
Glama
rankes96-dev

snow-mcp-server

by rankes96-dev
README.md
# Secure Delegated ServiceNow MCP Runtime

A secure MCP runtime on Google Cloud Run that enables a ServiceNow AI Agent to access another ServiceNow instance as the signed-in user—using delegated OAuth, least-privilege scopes, Dynamic Me filtering, Cloud KMS encryption, and rotating refresh tokens.

**Status:** Delegated read flow validated end to end; constrained approval-write path implemented and awaiting disposable sandbox validation<br>
**Stack:** TypeScript · Node.js · MCP · ServiceNow · Cloud Run · Firestore · Cloud KMS<br>
**Validation:** 94 automated tests passing

## Architecture

```mermaid
flowchart LR
    A["ServiceNow AI Agent<br/>MCP Client"]
    B["Secure MCP Runtime<br/>Google Cloud Run"]
    C["ServiceNow Resource Instance<br/>Delegated User Context"]
    D[("Firestore<br/>Sessions & Token Hashes")]
    E["Cloud KMS<br/>Encrypted Delegated Tokens"]

    A -->|"OAuth + PKCE<br/>MCP tools"| B
    B -->|"Delegated user bearer<br/>Allowlisted reads and approval transition"| C
    B --> D
    B --> E
```

The MCP bearer authenticates the client to this runtime and is never sent to ServiceNow. A separate delegated bearer represents the signed-in user to the ServiceNow resource instance, where Dynamic Me and ACLs enforce user-level access.

## What this project demonstrates

- Delegated identity across two ServiceNow instances.
- An enterprise MCP client/server integration with ServiceNow Agent Studio.
- Separation between the MCP client bearer and the downstream user bearer.
- Least-privilege tool scopes and server-owned query construction.
- Secure token storage, rotation, replay detection, and session revocation.
- Cloud-native deployment and automated validation on Google Cloud.

## Demo

The MCP server was authenticated from ServiceNow Agent Studio and exposed to a ServiceNow AI Agent through two read-only tools. The end-to-end flow was validated in the deployed environment:

```text
ServiceNow Agent Studio
→ MCP server on Google Cloud Run
→ delegated end-user OAuth
→ ServiceNow resource instance
→ user-scoped tool results
```

The AI Agent successfully invokes both read tools, and ServiceNow scopes the returned records to the signed-in user. This deployed demo did not validate a live approval write. The write tool is restricted to disposable sandbox/development validation until the Table API write is replaced by a transactional Scripted REST compare-and-update endpoint and the complete write flow is validated live.

## Implemented tools

### `get_work_items`

- Accepts `status`: `open` (default) or `all`.
- Accepts `limit`: integer from 1 to 100 (default 20).
- Reads incidents where `caller_id` or `opened_by` is the signed-in user.
- Reads requested items where `requested_for` or `opened_by` is the signed-in user.
- Adds `source_table`, `relationship_to_user`, and, for work opened for another person, the display name in `other_person`.
- Requires `mcp.work_items.read`.

### `get_pending_approvals`

- Accepts `limit`: integer from 1 to 100 (default 20).
- Returns only approvals whose state is `requested` and whose `approver` is the signed-in user.
- Projects canonical `state`, `source_table`, and timestamps from ServiceNow values while returning human-readable reference display values; reference SYS_IDs are not exposed.
- Fails closed when the `display_value=all` response is malformed or the approver has no usable display value.
- Requires `mcp.approvals.read`.

### `approve_pending_approval`

- Accepts only an exact approval `sys_id`, the previously observed update time, an optional `sys_mod_count`, and an idempotency key.
- The server fixes the table to `sysapproval_approver`, verifies Dynamic Me ownership and `requested` state, and permits only the transition to `approved`.
- Requires the separate `mcp.approvals.approve` scope; a read credential cannot invoke it.
- Uses a durable Firestore write claim. A known terminal result is replayed, while an ambiguous post-dispatch outcome is never retried automatically.
- ServiceNow Table API does not provide a documented atomic conditional-update primitive here, so the implementation reports its GET/PATCH stale-state check honestly (`atomic_compare_and_update: false`). Production hardening should use a narrow Scripted REST compare-and-update endpoint.
- This path is implemented and covered by mocked automated tests, but it has not been validated by mutating the deployed demo instance. Keep the write scope off production clients until the production hardening and live-validation gates are complete.

Tool inputs cannot select an ownership mode, table, field, encoded query, approver, user identifier, target state, or Dynamic Me filter ID. Responses contain only allowlisted ServiceNow fields and server-derived relationship metadata. See [the architecture](docs/architecture.md) for the fixed query plans and trust boundaries.

## Security highlights

- Authorization Code with canonical PKCE S256.
- Exact OAuth client, redirect URI, scope, and resource binding.
- Delegated ServiceNow execution as the signed-in user.
- Cloud KMS encryption with session- and purpose-bound contexts.
- Opaque MCP refresh tokens stored only as SHA-256 hashes.
- Atomic refresh-token rotation with replay-family session revocation.
- Fixed tables, fields, limits, sorts, and Dynamic Me query plans.
- No arbitrary encoded queries or user identifiers from the AI Agent.
- Separate read/write MCP scopes and durable at-most-once approval-write claims.
- HTTPS-only ServiceNow token exchange with redirects disabled.
- No access tokens, refresh tokens, authorization codes, record content, or query results in logs.

## OAuth flow

The MCP-facing flow supports Authorization Code with PKCE S256 and rotating refresh tokens for public clients:

1. The client calls `/oauth/authorize` with the canonical MCP `resource` and an explicit request for one or more supported tool scopes.
2. The runtime validates the OAuth client, exact redirect URI, resource, scopes, and canonical PKCE challenge, then redirects the browser to ServiceNow.
3. The callback exchanges the ServiceNow code and stores both delegated tokens encrypted in the MCP session.
4. The client exchanges the returned MCP authorization code, the same `resource`, and the PKCE verifier at `/oauth/token` for an MCP bearer lasting up to 15 minutes and a rotating refresh token.
5. Each tool call validates the MCP bearer, session, and required tool scope before using the delegated ServiceNow bearer for a fixed server-owned query or write transition.
6. The client renews access with `grant_type=refresh_token`, the same `client_id`, and the same `resource`. Each successful use rotates the MCP refresh token; the delegated ServiceNow token is refreshed only when needed.

Plaintext delegated tokens and plaintext MCP refresh tokens are never stored. Refresh-token reuse revokes the session. MCP refresh tokens expire after 30 days of inactivity, and each token family has a maximum lifetime of 90 days. Users authorize again only after expiry, revocation, or an invalid downstream refresh grant.

## Configuration

Required environment variables:

| Variable | Purpose |
| --- | --- |
| `SNOW_QA_URL` | HTTPS base URL of the ServiceNow resource instance. |
| `SNOW_QA_DELEGATED_CLIENT_ID` | Client ID of the delegated ServiceNow OAuth application. |
| `SNOW_QA_DELEGATED_CLIENT_SECRET` | Client secret of the delegated ServiceNow OAuth application. |
| `SNOW_QA_DYNAMIC_ME_FILTER_ID` | Sys ID that represent a Dynamic 'Me' Filter. |
| `MCP_TOKEN_KMS_KEY` | Full Cloud KMS key resource name used to encrypt delegated tokens. |
| `MCP_TOKEN_SIGNING_SECRET` | Base64-encoded MCP signing secret that decodes to at least 32 bytes. |

Optional environment variables:

| Variable | Default | Purpose |
| --- | --- | --- |
| `MCP_PUBLIC_URL` | `http://localhost:8080` | Public origin used for OAuth and MCP endpoint metadata. |
| `SNOW_QA_DELEGATED_SCOPE` | unset | Scope sent to the delegated ServiceNow authorization endpoint. |
| `PORT` | `8080` | HTTP listener port. |

The process also needs Google Application Default Credentials with access to the configured KMS key and Firestore database. OAuth clients are stored in `oauth_clients`; each active registration must allow only the tool scopes it needs.

## Local development

Node.js 20 or later is required. After setting the environment and Google credentials:

```shell
npm ci
npm run dev
```

To run the compiled server:

```shell
npm run build
npm start
```

## Validation

The automated tests use mocks and do not require live ServiceNow or GCP credentials:

```shell
npm run gcp-build
```

This command runs the TypeScript typecheck, all 94 automated tests, and the production build. For deployment changes, follow [the GCP migration checklist](docs/gcp-migration.md).

Maintenance

ActivityMaintained
ResponsivenessResponsive