Welcome to issue #01 of NowLink — a build-in-public newsletter of creating a local MCP server that connects Claude Desktop directly to ServiceNow.
This week we're covering everything from v0.0 to v0.1: secure OAuth setup, the data transformation layer that cuts token counts by 80%, and the silent authentication bug that cost an hour to diagnose. Let's get into it.
Why local MCP Server
Model Context Protocol (MCP) servers aren't a brand-new concept. But when you look at the current ecosystem of integrations connecting Large Language Models (LLMs) to enterprise platforms like ServiceNow, you quickly notice a pattern: most solutions want to route your requests through an external cloud proxy or a third-party middleman. For enterprise ecosystems, that's an immediate security roadblock.
NowLink is intentionally built differently: it is a 100% local MCP server. It runs entirely on your local machine, serving as a secure, direct bridge between your Claude Desktop client and your ServiceNow instance. Your credentials, your data payloads, and your session tokens never leave your hardware.
In this launch edition of our project newsletter, here is exactly what we are breaking down today:
The ServiceNow Identity Blueprint: The exact OAuth registry and user account specifications required to allow headless authentication on a PDI.
The Handshake Mechanics: How the v0.0 foundation securely automates local keychain credential storage and injects configurations directly into Claude Desktop.
The Magic of shaper.py: A deep dive into the v0.1 data transformation layer that filters heavy system payloads and slashes LLM context token counts by 80%.
The OAuth "Access Denied" Trap: A raw engineering log entry explaining a silent ServiceNow authentication checkmark that cost an hour to diagnose.
The Road to Extensibility: Our upcoming milestone plan for adding local, safe-mutation write features in v0.2.
Beyond security, it fixes the day-to-day developer friction of interacting with ServiceNow APIs. Instead of clicking through endless list views, piecing together complex encoded queries manually, or dealing with heavy JSON payloads containing 60 different fields when you only needed 5, a local server handles the heavy lifting. When you type: "Show me all P1 incidents without an assignment group," Claude reasons through the request locally, issues a scoped call to the server, and prints a perfectly clean, readable response directly in your chat window.
Phase 1: Secure Native Connection (v0.0) 🏗️
To prove out this architecture, the first milestone was establishing a secure, persistent linkage between the local runtime environment, Claude Desktop, and the cloud-hosted ServiceNow PDI without resorting to brittle plaintext configurations.
ServiceNow Identity & Access Control 🔐
To bypass standard cloud proxies, the server interfaces directly with ServiceNow’s native authentication endpoints using a specialized machine profile.
OAuth Registry Specification: Configured an Application Registry entry utilizing the Resource Owner Password Credential grant type, explicitly setting the client type as Integration as a Service.

OAuth Application Registry
Dedicated Integration Identity: Provisioned an explicit nowlink.dev user marked as a Machine identity and an Internal Integration User. This setup binds the engine to a baseline role set including rest_service, itil, and personalize_dictionary (the latter being critical to inspect table dict schemas). Note: uncheck the Password needs reset option.

Integration user

Integration user’s roles
Local Environment Integration 🦴
Local Keychain Encryption: Rather than letting sensitive OAuth secrets and integration passwords sit exposed in a raw text .env file where they risk getting accidentally committed to Git, NowLink leverages the OS-native keyring library to lock credentials down safely inside the machine's local keychain.
The CLI Engine (init & connect): Wired up clean terminal entry points using Typer. Running nowlink connect automatically locates and updates Claude Desktop’s native configuration file, explicitly parsing the virtual environment's executable path via sys.executable to ensure a flawless handshake.

NowLink init

NowLink connect
That’s it! No need of manual configuration file editing. After restarting of Claude, we can try to ping the ServiceNow instance.

Claude ping confirmation
We are connected!🤘
Phase 2: Tool Visibility & Read Access (v0.1) 📖
With the pipeline secured, the next challenge was teaching Claude how to fetch data safely without blowing past LLM context token limits or causing hallucinated fields.
The Data Transformation Engine (shaper.py) 🛡️
An LLM integration is only as good as the context data you feed it. Raw ServiceNow JSON responses routinely include 40–80 data points per record. The shaper.py layer acts as a local data transformation shield:
Forced Display Values: By passing sysparm_display_value=all on every outbound call, ServiceNow returns both raw IDs and string representations simultaneously. This allows immediate reference resolution (e.g., matching a user's name to their ID) without forcing secondary API roundtrips.
Allowlist Filtering: It aggressively drops heavy system metadata fields (like sys_mod_count), matches records to tight table-specific keys, and remaps numeric state indicators to legible plain-text string constants.
Core Read Capabilities Exposed to Claude
Three fundamental tools were deployed, exposing clean data models to the model:
query: For retrieving ordered lists of records with custom filtering conditions.

Using query tool in Claude
get_record: For pulling an exhaustive look at a singular target record with full references resolved.

Using get_record tool in Claude
describe_table: To let Claude dynamically inspect schema traits and understand field layouts on unknown ServiceNow tables.

Using describe_table tool in Claude
Under the Hood 🔍
How a tool actually looks like? It is a standard Python function containing detailed instructions WHEN and HOW the LLM should use it.
@mcp.tool()
def get_record(
table: str,
identifier: str,
) -> dict:
"""
Fetch a single ServiceNow record by its number or sys_id.
Use this tool when:
- The user asks about a specific record and provides a number (e.g. INC0001234, PRB0000012)
- You already know the sys_id of the record you need
- You need full detail on one specific record
Do NOT use this tool to search for records — use query instead.
Parameters:
- table: ServiceNow table name (e.g. "incident", "problem", "change_request")
- identifier: The record number (e.g. "INC0001234") or sys_id (32-character hex string).
Record numbers start with a prefix: INC=incident, PRB=problem, CHG=change_request,
REQ=sc_request, RITM=sc_req_item, TASK=task.
Returns a single shaped record with all relevant fields resolved to human-readable values.
"""
params = {"table": table, "identifier": identifier}
try:
# sys_id is a 32-char hex string; numbers have letter prefixes
if len(identifier) == 32 and all(c in "0123456789abcdef" for c in identifier.lower()):
raw = get_record_by_sys_id(table, identifier)
else:
raw = get_record_by_number(table, identifier)
shaped = shape_record(raw, table)
log_tool_call("get_record", params, f"returned {identifier}")
return shaped
except Exception as e:
log_error("get_record", params, str(e))
return {"error": str(e)}The Weekly Gotcha 🪵
Building local-first integrations means wrestling directly with enterprise authentication quirks. During the initial setup of the Resource Owner Password Credential grant, the local CLI repeatedly threw a generic access_denied error from the ServiceNow OAuth endpoint.
The application registry parameters matched, permissions were solid, and the required API roles (rest_service, itil) were attached. Administrative credentials connected instantly, yet the dedicated integration user account failed silently without providing logs.
The Root Cause: The "Password needs reset" checkbox on the ServiceNow User record.

When creating an integration user manually, ServiceNow frequently leaves this checked by default. While a human user hits a clean web browser redirect forcing a password update, a headless machine identity attempting an OAuth token swap simply breaks completely without returning an error context. Unchecking it fixed the token exchange immediately. Keep an eye out for this setting whenever configuring external programmatic access.
The Extensibility Horizon: Where NowLink is Headed
The ultimate destination for NowLink isn't just a static set of pre-packaged tools; it is complete extensibility.
Next Milestone (v0.2): Write Capabilities. Giving Claude the ability to securely create and update records locally, backed by local append-only audit logging (~/.nowlink/logs/) to track every change state before it executes in the cloud.
The Long-Term Play (v0.5): Custom Tool API. Building an open plugin layer that allows any developer to write a custom tool in under 30 lines of Python, register it with a single terminal command, and have it surface with instant visibility inside Claude Desktop.
If this was useful, forward it to one person fighting with ServiceNow API responses.
Until next week, happy coding.
- Tomas

