Barebones example showing how AI tools can call a custom ServiceNow API instead of connecting directly to the Table API.
The ServiceNow side is only two files:
- a Script Include with the tools/actions
- a Scripted REST API resource that calls that Script Include
MCP path:
AI tool → MCP server → Scripted REST API resource → Script Include tools → ServiceNow
SDK/app path:
AI or app workflow → Connector.fetch(...) → Scripted REST API resource → Script Include tools → ServiceNow
The client interface is not the point.
The custom API is the boundary for what AI can read, write, execute, log, and expose.
.
├── src/index.ts
├── servicenow/01-copy-into-servicenow-script-include-Custom_ServiceNow_API.js
├── servicenow/02-copy-into-servicenow-scripted-rest-resource-custom_api_POST.js
├── .env.example
├── package.json
└── README.md
MCP tools:
sn_query— query tables through the custom APIsn_get— get one record bysys_idsn_schema— inspect a table schemasn_script— server-side script execution, enabled by default
sn_script is included by default.
That tool is powerful because it gives the AI a controlled path to ServiceNow server-side JavaScript and GlideRecord.
That is also why it is dangerous. Use it only in a controlled dev/admin context. The real access is whatever your integration user and Script Include allow.
npm install
npm run buildThis repo includes the Script Include code here:
servicenow/01-copy-into-servicenow-script-include-Custom_ServiceNow_API.js
Create it in ServiceNow:
- Open a dev or sub-prod ServiceNow instance.
- Go to
System Definition > Script Includes. - Click
New. - Set:
Name:Custom_ServiceNow_APIActive: checkedClient callable: uncheckedAccessible from:This application scope onlyunless you intentionally need cross-scope access
- Replace the generated script with the full contents of:
servicenow/01-copy-into-servicenow-script-include-Custom_ServiceNow_API.js
- Save or submit.
No ServiceNow properties are required for the demo.
The defaults are in the Script Include:
sn_scriptenabled- max query limit
500 - all tables allowed by default, limited by the integration user's roles and ACLs
To restrict tables, uncomment the per-table block in initialize():
// this.allowedTables = this._csv([
// 'incident',
// 'problem',
// 'change_request',
// 'sys_script',
// 'sys_script_include',
// 'sys_db_object',
// 'sys_dictionary'
// ].join(','));If you want read-only discovery, set this.enableScript = false in the Script Include and set SN_ENABLE_SCRIPT=false in the MCP config.
Start in dev or sub-prod.
This repo includes the REST resource code here:
servicenow/02-copy-into-servicenow-scripted-rest-resource-custom_api_POST.js
Create the API in ServiceNow:
- Go to
System Web Services > Scripted REST APIs. - Click
New. - Set:
Name:ServiceNow Custom APIAPI ID:custom_apiActive: checked
- Save or submit.
ServiceNow will expose the API under this shape:
/api/<your_namespace>/custom_api
For a scoped app, <your_namespace> is usually your app scope, for example:
/api/x_your_scope/custom_api
Now create the POST resource:
- Open the
ServiceNow Custom APIScripted REST API record. - In the
Resourcesrelated list, clickNew. - Set:
Name:POSTHTTP method:POSTRelative path:/Requires authentication: checked
- Paste the full contents of this repo file into the resource script:
servicenow/02-copy-into-servicenow-scripted-rest-resource-custom_api_POST.js
- Save or submit.
If your ServiceNow version does not allow / as the resource path, use /run instead. Then your endpoint becomes:
/api/<your_namespace>/custom_api/run
Use that full URL as SN_API_URL.
The MCP server sends JSON like:
{
"action": "query",
"table": "incident",
"query": "active=true",
"limit": 5
}The Scripted REST API calls the Script Include: Custom_ServiceNow_API.
Quick test with basic auth:
curl -u "$SN_USER:$SN_PASS" \
-H "Content-Type: application/json" \
-X POST "$SN_API_URL" \
-d '{"action":"query","table":"incident","query":"active=true","limit":1}'Expected response shape:
{
"success": true,
"result": {
"table": "incident",
"count": 1,
"records": [
{
"sys_id": "...",
"number": "INC0010001"
}
]
}
}Use a dedicated integration user.
Do not use your personal admin account.
Keep its roles narrow. Your custom API is not a replacement for ServiceNow ACLs. It is an extra control layer.
Copy .env.example into your shell, launcher, or MCP client config.
Basic auth:
export SN_API_URL="https://YOUR_INSTANCE.service-now.com/api/x_your_scope/custom_api"
export SN_USER="<integration_user>"
export SN_PASS="<password>"
export SN_ENABLE_SCRIPT="true"Bearer token:
export SN_API_URL="https://YOUR_INSTANCE.service-now.com/api/x_your_scope/custom_api"
export SN_BEARER_TOKEN="<token>"
export SN_ENABLE_SCRIPT="true"node dist/index.jsFor an MCP client, point the client at:
node /absolute/path/to/servicenow-custom-api-example/dist/index.js
Example MCP config shape:
{
"mcpServers": {
"servicenow-custom-api": {
"command": "node",
"args": ["/absolute/path/to/servicenow-custom-api-example/dist/index.js"],
"env": {
"SN_API_URL": "https://YOUR_INSTANCE.service-now.com/api/x_your_scope/custom_api",
"SN_USER": "<integration_user>",
"SN_PASS": "<password>",
"SN_ENABLE_SCRIPT": "true"
}
}
}
}Do not commit real credentials.
MCP is just one interface.
The same custom API can be called from other controlled paths, including SDK / Fluent-style code using Connector.fetch(...).
Shape:
await Connector.fetch({
method: 'POST',
url: '/api/x_your_scope/custom_api',
body: {
action: 'query',
table: 'incident',
query: 'active=true',
limit: 5
}
});Exact syntax depends on where you are running it. The point is not the client. The point is the custom API boundary.
Script execution is on by default in this example. To disable it:
Local MCP env:
export SN_ENABLE_SCRIPT="false"ServiceNow Script Include:
this.enableScript = false;Example script:
var gr = new GlideRecord('incident');
gr.addActiveQuery();
gr.setLimit(5);
gr.query();
var out = [];
while (gr.next()) {
out.push({
number: gr.getValue('number'),
short_description: gr.getValue('short_description')
});
}
out;Warning: server-side script execution can read, write, or delete data depending on the API user's roles and your Script Include. Treat it as privileged access.
This repo is an example, not a production security product.
Before using the pattern seriously, add:
- tighter table allowlists
- field allowlists
- explicit write gates
- audit logging
- request IDs
- rate limits
- environment checks
- no production writeback by default
- approvals for dangerous actions
- separate controls for script execution
You do not need to give an AI agent broad Table API access just because you want it to work with ServiceNow.
Use MCP, Connector.fetch, or another client interface if you want.
But put your own custom API in the middle when the organization needs control over data, access, and governance.