Skip to main content
Glama

Open Now — Headless MCP for ServiceNow

Open Now is a headless MCP interface for ServiceNow: a small, stable MCP kernel, a curated skill catalog that encodes how real work is done (ITSM/CMDB/ITOM/SPM/CSM/HRSD/SecOps/platform), a generated toolkit for metadata-driven table/record/script access, and an in-instance runtime (sn_headless scoped app) that keeps every read and write inside ServiceNow's ACL, Business Rule, and Data Policy fabric.

The invariant: the AI client orchestrates; ServiceNow remains the governed system of action. The kernel is protocol only — all capability, permission, and audit logic runs in the instance as the invoking user. No Table API in the kernel, no service account on the interactive path.

Design spec: docs/spec.md · Wire contract: docs/wire-contract.md · Implementation plan: docs/implementation-plan.md


How it fits together

flowchart LR
    subgraph Client["Any MCP client (Claude, Copilot, Grok, CLI...)"]
        C1[discover / describe / dispatch_readonly / dispatch]
    end
    subgraph Kernel["open-now mcp-server (Bun/TS)"]
        T[4 stable kernel tools + toolkit + confirm policy + OAuth PKCE session]
    end
    subgraph Instance["ServiceNow instance"]
        REST[Scripted REST sn_headless]
        RT[SkillRuntime + QueryGuard + RecordResolver + ConfirmGate]
        EXEC[Skill executables - Script Includes]
        GLIDE[GlideRecordSecure = real ACLs, Business Rules, Data Policies, Flows]
    end
    C1 -- MCP --> T -- token-bound REST, as user --> REST --> RT --> EXEC --> GLIDE

Three components, one contract (packages/contracts):

Component

Path

What it owns

MCP kernel

packages/mcp-server

The four tools, confirmation protocol, OAuth/PKCE, sessions, domain modes

Instance runtime

instance/sn_headless

Skill registry, audit, discover index, query guard, planners/appliers (scoped app update set)

Skill catalog

packages/skill-docs

32 validated skill documents (the contract source of truth; seeded into the registry)


Related MCP server: servicenow-platform-mcp

Quickstart (PDI or subprod)

  1. Build the appbun run build:app produces dist/sn_headless/update-set.xml.

  2. Install it — System Update Sets → Load XML, then activate the sn_headless scope. Full PDI walkthrough: instance/bootstrap/README.md.

  3. Create an OAuth app in the instance (System OAuth → Application Registry): grant type Authorization Code, PKCE enabled, redirect URI = your server URL + /oauth/callback. Copy the client id.

  4. Configure the server (env or --config file.json; see Configuration).

  5. Seed the catalogbun run seed (imports all 32 skill documents into the registry; --dry-run to preview).

  6. Run — stdio for local/builder use, HTTP for interactive clients:

    # stdio (static per-user token)
    OPEN_NOW_INSTANCE_URL=https://dev123456.service-now.com \
    OPEN_NOW_ACCESS_TOKEN=<your-token> \
    bun run packages/mcp-server/src/bin/open-now.ts --transport stdio
    
    # http (per-user OAuth sign-in)
    OPEN_NOW_INSTANCE_URL=https://dev123456.service-now.com \
    OPEN_NOW_OAUTH_CLIENT_ID=<client-id> \
    bun run packages/mcp-server/src/bin/open-now.ts --transport http
  7. Connect a client — e.g. Claude Desktop (claude_desktop_config.json):

    {
      "mcpServers": {
        "open-now": {
          "command": "bun",
          "args": ["run", "packages/mcp-server/src/bin/open-now.ts", "--transport", "stdio"],
          "env": {
            "OPEN_NOW_INSTANCE_URL": "https://dev123456.service-now.com",
            "OPEN_NOW_ACCESS_TOKEN": "<your-token>"
          }
        }
      }
    }

    In HTTP mode the client signs in at /oauth/authorize; each MCP session is bound to the resulting user token. Pass the session via the Mcp-Session-Id header or Authorization: Bearer <sessionId> (the /oauth/token endpoint mints the session id as an opaque token).

One quick check

bun run eval -- --gateway mock   # 9 golden steps, no instance required

Using the tools

The kernel tool surface is intentionally four stable tools (spec §3.3); the action surface grows behind them — via the 32-skill catalog and the generated toolkit (see below) — so the model context never has to absorb hundreds of tool descriptions.

Default surface: 4 kernel tools + 32 curated skills + 10 generated toolkit tools. In table mode (OPEN_NOW_TOOLKIT=table) the toolkit expands to ~125 per-table tools; everything still routes through one instance-side trust boundary.

Tool

Purpose

discover

Semantic/intent search over the skill index. Returns ranked skill ids + raw-operation fallbacks with a one-line why.

describe

The technical contract: inputs, tables, roles, confirmation policy, related skills. Required before dispatch for write-class skills.

dispatch_readonly

GET-equivalent only. Reads, aggregates, relationship walks, KB search. Never mutates.

dispatch

Invoke a skill. Confirmation policy enforced: write-class returns a pending diff unless confirm: true with the same requestId; then it applies exactly once. Raw operations use skillId: "raw:<table>".

Example flow

discover("server down for the app team")
  → sn.cmdb.ci.find [0.9], sn.itsm.incident.triage [0.8], raw:incident [0.05]

describe("sn.itsm.incident.update")
  → { inputs: { incident_number, mode: comment|work_note|resolve, ... }, confirmation: update_owned, ... }

dispatch("sn.itsm.incident.update", { inputs: { incident_number: "INC0010001",
           mode: "resolve", resolution_code: "fixed", resolution_notes: "rebooted" } })
  → { outcome: "pending", diff: [ { field: "state", before: "1", after: "6" }, ... ],
      auditId: "...", requestId needed for confirm }

dispatch("sn.itsm.incident.update", { inputs: {...}, confirm: true, requestId: "<same id>" })
  → { outcome: "applied", focusedPayload: { record_numbers: ["INC0010001"] } }

Write semantics:

  • read — answered directly, never confirmed.

  • update_owned + autoApply policy + record owned by caller — applied silently (policy is declared in the skill document; the kernel/registry enforce it).

  • everything else — pending with a field diff (or draft for create-class) until confirmed.

  • resolve mode requires resolution_code + resolution_notes; missing inputs come back as outcome: "error" with missingFields.

  • same requestId twice → Server replays the settled outcome; the applier never runs twice.

  • GraphQL-shaped risk: HR/SecOps skills take structured inputs only — an itil user without sn_hr_core.case_writer gets denied before any HR-table query runs.

Raw fallback

dispatch on raw:incident ({ table, query, fields, limit }) is the builder escape hatch behind QueryGuard (allowlisted operators, capped windows, field allowlists, no JS:/GOTO). It always shows as a discover candidate but is never the default path for operators.

Toolkit — the generated wide surface

Beyond the 32 skills, a generated toolkit covers any table without hand-written glue — all still executed in the instance through QueryGuard/RecordResolver/ConfirmGate/audit. No Table API in the kernel, no service account, ever.

Generic mode (default, OPEN_NOW_TOOLKIT=generic) — 10 tools, context-safe, metadata-driven:

Tool

Behavior

table_list / table_schema

metadata from sys_db_object/sys_dictionary

record_get

one record by number or sys_id (role-gated on HR/SecOps tables)

record_create / record_update / record_delete

draft/diff first → confirm:true + same requestId applies exactly once; deletes are restricted (never silent)

aggregate_report

server-side COUNT/AVG/MIN/MAX/SUM, optional groupBy — no row dumps

run_script

short server-side Glide script, eval'd in the scoped runtime

attachment_list / attachment_add

attachments per record (base64, draft-then-confirm)

Table mode (OPEN_NOW_TOOLKIT=table) — NowAIKit-style breadth: tbl_<table>_{query,get,create,update,delete} generated per table from OPEN_NOW_TABLE_TOOLS (comma list; default is a core ~25-table allowlist → ~125 tools). Everything routes to the same runtime — one audit model, same confirm semantics. Set OPEN_NOW_TABLE_TOOLS= to disable, or list exactly the tables you want.

table_schema({ table: "incident" })        → fields from sys_dictionary
record_update({ table: "incident", number: "INC0010001", values: { state: "2" } })
  → pending (field diff) → confirm:true + same requestId → applied (exactly once)
record_delete({ table: "incident", number: "INC0010001" })
  → pending ("Deletes require confirmation") → confirm:true → deleted
run_script({ script: "new GlideRecord('incident').getRowCount()" }) → result

Skill catalog

32 skills ship today (all validated against the contracts schema; every doc has a matching Exec* planner + applier — enforced by tests):

  • P1 (22): shift briefing, incident triage/similar/update/major, problem open, change draft/assess-risk/CAB-prep/implement, request submit/fulfill, SLA at-risk, ITOM alert triage/correlate, CMDB CI find/blast-radius/service health, SPM portfolio, KB answer, my work, safe aggregate.

  • P2 (10): SPM project prep, CSM case briefing/update, HRSD case handle, SecOps SIR/vuln, platform schema describe, update set review, script impact, flow run.

Adding a skill: create packages/skill-docs/src/sn.<domain>.<obj>.<verb>.json (strict schema — SkillDoc in packages/contracts; one executable ref naming the domain Exec* class and plan_/apply_ methods), implement the method in the matching instance/sn_headless/script-includes/Exec*.js, re-run bun run build:app && bun run seed. bun test tests/unit/skill-docs.test.ts + tests/unit/instance/exec-methods.test.ts verify the binding.


Configuration

All settings via environment (defaults in parentheses) or --config file.json (same keys, camelCase):

Env var

Purpose

OPEN_NOW_INSTANCE_URL

Instance URL, no trailing slash (required; SNOW_INSTANCE also accepted)

OPEN_NOW_TRANSPORT

stdio (default) or http

OPEN_NOW_PORT

HTTP port (default 8787)

OPEN_NOW_ACCESS_TOKEN

Static per-user token for stdio mode (SNOW_ACCESS_TOKEN alias)

OPEN_NOW_OAUTH_CLIENT_ID

OAuth application registry client id (http mode)

OPEN_NOW_OAUTH_REDIRECT_URI

Defaults to http://localhost:8787/oauth/callback

OPEN_NOW_OAUTH_SCOPES

Comma-separated, default useraccounts

OPEN_NOW_DB_PATH

SQLite path for session store (default in-memory)

OPEN_NOW_DOMAIN

Domain mode: expose only sn.<domain>.* skills as <domain>_<slug> tools (e.g. itsm)

OPEN_NOW_REQUIRE_DESCRIBE

Hard-require describe before any dispatch

OPEN_NOW_ENABLED_SKILLS

Comma-separated allowlist

OPEN_NOW_TOOLKIT

Generated wide surface: generic (default) | table | off

OPEN_NOW_TABLE_TOOLS

Comma-separated table allowlist for table mode (empty = none; unset = core allowlist)

Pause switch: each registry row has an active flag — a single flip pauses a skill sans deploy.


Development

bun install
bunx tsc --noEmit                      # typecheck
bun test tests/unit                    # 74 unit tests (kernel, runtime shim, catalog, builder)
bun run eval -- --gateway mock         # golden protocol suite (9 steps, §8.4 cases)
bun run eval -- --gateway instance     # same suite against a real instance (env-gated)
bun run bench                          # latency + token estimate per skill
bun run seed -- --dry-run              # catalog import preview
bun run build:app                      # rebuild dist/sn_headless/update-set.xml

CI (.github/workflows/ci.yml) runs typecheck + unit tests + app build on every push; the integration job activates when SNOW_INSTANCE/SNOW_ACCESS_TOKEN secrets are present.

Instance-side tests use a Glide shim (tests/unit/helpers/glide-shim.ts) — no instance needed. Real ACL/BR verification runs via bun test tests/integration + --gateway instance against a PDI (see instance/bootstrap/README.md).


Security model (short version)

  • Default run-as is the invoking user; no impersonation, per-user OAuth + PKCE.

  • Writes fire real Business Rules/Data Policies/Approvals in the instance — the MCP layer never reimplements them.

  • Every dispatch writes an audit row (sn_headless_run): actor, skill, tables, record numbers, query hash, outcome, latency. A dispatch without an audit line is a failed dispatch.

  • Model queries are hostile: QueryGuard allowlists operators/caps/fields, rejects JS:/GOTO.

  • Restricted tables (HR/SecOps/Legal) deny by role before any query; deletes are opt-in and off by default.

  • 403 is fail-closed: the skill returns denied with a reason; the client never retries with a broader query.


Repository layout

open-now/
├── README.md                       # this file (usage)
├── docs/                           # spec.md (design), wire-contract.md, evaluation.md, implementation-plan.md
├── packages/
│   ├── contracts/                  # zod schemas + types (single source for both sides)
│   ├── skill-docs/                 # 32 skill documents (catalog source of truth)
│   ├── mcp-server/                 # kernel, gateways, auth, domain modes, CLI
│   └── client-lib/                 # typed client helpers for MCP hosts
├── instance/
│   ├── sn_headless/                # scoped app: app.json, script-includes/, rest/
│   └── bootstrap/                  # PDI setup guide
├── scripts/                        # build-app, seed, eval-runner, bench, export-update-set
├── tests/                          # unit/, integration/, fixtures/ (goldens, mock gateway)
└── dist/                           # built update set (git-ignored)

Related MCP Connectors

Related MCP Servers