Open Now
by syndicalt
README.md
# 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`](docs/spec.md) · Wire contract: [`docs/wire-contract.md`](docs/wire-contract.md) · Implementation plan: [`docs/implementation-plan.md`](docs/implementation-plan.md)
---
## How it fits together
```mermaid
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) |
---
## Quickstart (PDI or subprod)
1. **Build the app** — `bun 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`](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](#configuration)).
5. **Seed the catalog** — `bun 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:
```bash
# 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`):
```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
```bash
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
```text
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.
```text
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
```bash
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`](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)
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues