- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
I have built a handful of MCP servers off platform now, and the shape of it is always the same. Spin up a Node project. Wire in the MCP SDK. Figure out auth, which is never as simple as the docs make it look. Then find somewhere to actually run the thing, so now you are picking a host, standing up a deploy pipeline, and maintaining a second project that has nothing to do with the problem you sat down to solve. Somewhere near the end of all that, you finally get to write the part that talks to ServiceNow.
Then I built the same kind of server on ServiceNow instead, and most of that list just stopped being my problem. No scaffolding. No host. No pipeline. And auth was the same OAuth setup I have done a hundred times for reasons that had nothing to do with AI.
This is the series where we build one together.
Table of Contents
Chapter 2: What Is an MCP Server?
Chapter 3: Anatomy of an MCP Server on ServiceNow
Chapter 4: How to Implement an MCP Server on ServiceNow
- Setup: instance, plugin, verify
- Build the GET resource
- Build the POST resource and schema
- Set up the OAuth application registry
- Build the MCP tools
- Build the MCP server
- Connect Claude
- Test the MCP server
Chapter 1: Why This Series
I'm Travis Toulson, Senior Developer Advocate at ServiceNow, and this series is about connecting third party AI assistants to your instance using ServiceNow's native MCP server capabilities.
Why now? ServiceNow shipped the MCP Server Console as part of Action Fabric late last year, and this past May added the REST API tool, which is the one this build uses.
Why bother? Before this, giving an AI assistant access to the data and processes in your instance meant building and hosting an MCP server yourself, or copying data out of ServiceNow by hand and pasting it into a chat.
Now the server runs on the platform. Claude authenticates as a real user through an OAuth registry you created, it can only call the tools you put on the server, and everything it does happens inside your instance under your ACLs.
The hard part of building one is not the configuration. It is deciding which tools to expose and how to describe them, because those descriptions are what the model reads when it decides what to do. That comes up repeatedly once the building starts.
Each episode runs about two to three minutes. Watch them in order or jump to whatever you need right now. Everything in the videos is also written out on this page, scripts included, so if reading is faster for you then skip the videos and work straight from here.
And if there is something you want covered on MCP in ServiceNow, tell me in the comments and I will add an episode.
Chapter 2: What Is an MCP Server?
MCP stands for Model Context Protocol. It is a standard way for an AI model to talk to software outside of the model. Software like, say, ServiceNow.
Here is the problem it solves. A model like Claude is very good at reasoning, writing, and figuring out what needs to happen next. What it is not good at, on its own, is taking action. It cannot log into your instance. It cannot run a query. It cannot click a button. MCP is how it gets to do those things.
There are three pieces worth knowing.
The client is the AI application itself. Claude, in our case. The thing a person is actually chatting with.
ServiceNow can be a client too. Build Agent connects out to third party MCP servers like Figma's. This series goes the other direction, with ServiceNow as the server.
The server is the program that sits between the AI and the software. It exposes a specific set of capabilities to the AI, and it knows how to go do the work in that software when asked.
The tools are the individual actions the server offers up, like getting the details of an incident or updating a record. Each one has a name, a description, and a defined set of inputs.
Client, server, tool. Those three words are most of what you need to follow the rest of this series.
When you are chatting with Claude and it needs to do something real, it looks at the tools available, decides which one fits what you asked for, fills in the inputs, and asks the server to run it. That is the whole mechanic.
Chapter 3: Anatomy of an MCP Server on ServiceNow
Now let's see how those concepts land as actual records and metadata on the platform. There are four pieces you will keep running into.
The OAuth application registry handles authentication. It is what lets an AI client log into your instance securely, under a scope that limits what it is allowed to touch.
The MCP server record is the thing you stand up and expose. It has a URL and a list of tools it is willing to offer.
The MCP tool record gives the client the ability to perform one specific action. Name, description, inputs. Those three things are exactly what the model reads to decide when and how to use it.
The backing implementation is whatever actually does the work. In this walkthrough it is a Scripted REST API. It does not have to be. Flow action, Now Assist skill, knowledge graph, subflow. The tool does not care what is behind it as long as it gets an answer back.
Once that picture is in your head, the rest of this is filling in details.
Chapter 4: How to Implement an MCP Server on ServiceNow
We are building a server that lets Claude read and update incidents by number. Two tools, two REST resources, one OAuth registry, one server.
What you need before you start
- An instance on Australia Patch 2 or later (the REST API tool needs it)
- Now Assist Admin Console (
sn_nowassist_admin) installed. Confirm this before you start, because the MCP Server plugin may not install cleanly without it. Other Now Assist dependencies come along automatically. - The admin role
- A Claude.ai account, or another MCP-compatible client
Step 1: Getting your instance ready
Install the plugin.
- Navigate to All > System Applications > All Available Applications > All
- Search for Model Context Protocol Server (App ID
sn_mcp_server). - Install it.
Verify it actually worked.
Do this before you build anything on top of it.
- Type
sys_service.listin the filter navigator - Search for MCP in the name column.
- You should see two records:
MCP-Sandmcp-server. - Open the
mcp-serverrecord - Check the Service Endpoints related list. There should be a record with Active set to true and a URL of
https://mcps-prod-default. - Then hit the health endpoint from your terminal:
curl https://<your-instance>.service-now.com/sncapps/mcp-server/health
You want this back:
{"status":"healthy"}
Anything else means the install didn't quite go as planned. You'll need to fix that before you go further. And if you need help, feel free to reach out. I don't have all the answers but I do like trying to find them.
Create a scoped app.
Everything you build in this guide should live in one scope.
- Go to All > System Applications > Studio and create a new scoped application, something like
Incident MCP Server. I started mine in the ServiceNow IDE and then built and installed it so it showed up platform-side, but use whatever environment you like. - Make sure your app is selected in the application picker before you create anything else. Every record in the following steps needs to be in that scope, including the tools and the server.
Step 2: Build the GET resource
Every MCP tool needs something to call. Ours is a Scripted REST API.
- Navigate to All > System Web Services > Scripted REST APIs and click New.
| Field | Value |
|---|---|
| Name | Incident API |
| API ID | incident_api |
| Default ACLs | Scripted REST External Default |
- Save from the hamburger menu (⋮) so the form reloads with the related lists visible.
- Now click New in the Resources related list.
- Complete the form as follows:
| Field | Value |
|---|---|
| Name | Get Incident |
| HTTP Method | GET |
| Relative Path | /{number} |
| Default ACLs | Scripted REST External Default |
Script:
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var number = request.pathParams.number;
if (!number) {
response.setStatus(400);
response.setBody({ message: "Incident number is required." });
return;
}
var gr = new GlideRecord('incident');
gr.addQuery('number', number);
gr.query();
if (gr.next()) {
response.setStatus(200);
response.setBody({
sys_id: gr.getValue('sys_id'),
number: gr.getValue('number'),
short_description: gr.getValue('short_description'),
description: gr.getValue('description'),
state: gr.getValue('state'),
urgency: gr.getValue('urgency'),
priority: gr.getValue('priority'),
assigned_to: gr.getDisplayValue('assigned_to'),
assignment_group: gr.getDisplayValue('assignment_group'),
opened_at: gr.getValue('opened_at'),
caller_id: gr.getDisplayValue('caller_id')
});
} else {
response.setStatus(404);
response.setBody({ message: "Incident not found: " + number });
}
})(request, response);
- Click Submit.
Now here is the part I actually care about, and it is the thing I want you carrying through the rest of this build.
That {number} path parameter is going to surface automatically as a required input on the MCP tool. No schema record needed for GET. Which means the name you gave it becomes part of what the model reads when it decides how to call your tool.
The dynamic content you send to REST APIs, path parameters and POST bodies, is not just for your own REST client anymore. It is something a model can see, interpret, and decide to fill on its own. This still looks like a normal REST resource, and structurally it is one. But it is now doing double duty as the back half of an AI-facing interface, and the AI cannot email you asking what you meant by p1.
Step 3: Build the POST resource
Reading is half the job. Let's let Claude make changes.
- Click New in the Resources related list again.
- Complete the form as follows:
| Field | Value |
|---|---|
| Name | Update Incident |
| HTTP Method | POST |
| Relative Path | /{number} |
| Default ACLs | Scripted REST External Default |
Script:
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var number = request.pathParams.number;
if (!number) {
response.setStatus(400);
response.setBody({ message: "Incident number is required." });
return;
}
try {
var params = request.body.data;
var gr = new GlideRecord('incident');
gr.addQuery('number', number);
gr.query();
if (!gr.next()) {
response.setStatus(404);
response.setBody({ message: "Incident not found: " + number });
return;
}
for (var key in params) {
if (params.hasOwnProperty(key)) {
if (key === 'work_notes' || key === 'comments') {
gr[key] = params[key];
} else {
gr.setValue(key, params[key]);
}
}
}
gr.update();
response.setStatus(200);
response.setBody({
message: "Incident updated successfully.",
number: gr.getValue('number'),
sys_id: gr.getUniqueValue()
});
} catch (e) {
response.setStatus(500);
response.setBody({ message: "Error occurred: " + e.message });
}
})(request, response);
Now the schema.
POST resources take a body in addition to URL parameters, and the MCP server needs to know what is supposed to go in there. What properties does the body support? What types? What do those properties mean to a model trying to populate them? Which ones are required?
For that you need an OpenAPI schema. Skip it and the tool still gets created, but it comes through with no body inputs, which leaves the model nothing to fill in and your resource nothing to work with.
- From the Incident API record, click New in the Schemas related list.
- Complete the form as follows:
| Field | Value |
|---|---|
| Name | UpdateIncidentSchema |
| API | Incident API |
| OpenAPI Version | 3.0.1 |
Schema:
{
"type": "object",
"description": "Fields to update on the incident. All fields are optional. Include only what you want to change.",
"properties": {
"short_description": {
"type": "string",
"description": "Brief summary of the incident.",
"example": "User cannot log in to VPN"
},
"description": {
"type": "string",
"description": "Detailed description of the incident.",
"example": "User reports being unable to connect to VPN since this morning."
},
"urgency": {
"type": "string",
"description": "Urgency code. 1=High, 2=Medium, 3=Low.",
"example": "2"
},
"impact": {
"type": "string",
"description": "Impact code. 1=High, 2=Medium, 3=Low.",
"example": "2"
},
"state": {
"type": "string",
"description": "Incident state code. 1=New, 2=In Progress, 6=Resolved, 7=Closed.",
"example": "2"
},
"assignment_group": {
"type": "string",
"description": "sys_id or name of the group to assign the incident to.",
"example": "Service Desk"
},
"assigned_to": {
"type": "string",
"description": "sys_id or username of the person to assign the incident to.",
"example": "john.smith"
},
"work_notes": {
"type": "string",
"description": "Internal notes to add to the incident work notes journal. Visible to agents only.",
"example": "Investigated and escalated to network team."
},
"comments": {
"type": "string",
"description": "Customer-visible notes to add to the incident comments journal. Visible to the caller and agents.",
"example": "We are investigating your issue and will update you shortly."
}
}
}
Look at the urgency property for a second. 1=High, 2=Medium, 3=Low is in the description because otherwise the model has no idea that "set this to high" means sending the string "1". Same reason every property has an example. That schema is the only briefing the model gets.
Full disclosure on how that JSON got written: I had Claude generate it. Your mileage may vary, and you should read what it produces before you trust it, but I am not going to pretend I hand-authored an OpenAPI blob, or that I spent any time learning what goes into one.
Then we need to link our schema to our POST resource.
- Open the Update Incident resource
- Click New in the Request Schema related list
- Set Schema to
UpdateIncidentSchema - Click Submit
Step 4: Configure OAuth
None of what you just built matters if Claude cannot log in.
- Go to All > System OAuth > Application Registry
- Click New.
- On the interceptor page, choose [Deprecated UI] Create an OAuth API endpoint for external clients. Yes, it says deprecated. Yes, it is the one you want. Ignore the label.
- Complete the form as follows:
| Field | Value |
|---|---|
| Name | Claude MCP Connector |
| Redirect URL | https://claude.ai/api/mcp/auth_callback |
| Token Format | JWT |
- In the Auth Scopes embedded list at the bottom, type
useraccount. - Save from the hamburger menu.
- Copy the Client ID and Client Secret somewhere. You need both when we connect Claude later in the build.
Token Format is the one that will get you.
If you leave it on the default, Opaque, the authentication fails at connection time. It shows up as a connector that connects fine but lists zero tools. This is the step I forgot almost every time I set this up. Make sure you set it to JWT.
The redirect URL above is Claude's. If you are using a different client, get the callback URL from that provider's docs, and check the rest of this form against them too. Other settings on it may need to change.
On the useraccount scope. I am using it here because it is the fast path, but be clear about what it does. It grants the client broad access to every REST API in the instance under the authenticated user's permissions. The MCP server limits what the AI can actually reach through your tools, but the token itself is wide. For anything past a lab exercise, build a custom auth scope with tighter restrictions. I am telling you what I did, not necessarily telling you it is what you should ship.
Step 5: Build the MCP tools
- Navigate to All > Admin Center > MCP Server Console.
- Click the Tools tab
- Click Create Tool
- Select REST API on the category screen.
- In the endpoint search field, type
/xto filter down to your scoped endpoints and select[GET] /x_snc_<your_scope>/incident_api/{number}. - Complete the form as follows:
| Field | Value |
|---|---|
| Label | Get Incident |
| Description | Retrieves the details of a specific ServiceNow incident by incident number. Call this tool when the user wants to look up, check, or get information about a specific incident. |
- Click Create
- Use the Tools breadcrumb to get back to the tools list
- Repeat steps 3-6 for the POST endpoint with the following values:
| Field | Value |
|---|---|
| Label | Update Incident |
| Description | Updates fields on an existing ServiceNow incident by incident number. Call this tool when the user wants to change, modify, or update an incident. Pass only the fields you want to change along with the required incident number. |
Two things to notice on the tool forms.
The GET tool has exactly one input, number, picked up straight from the path parameter. The POST tool has a pile of them, because it inherited the path parameter and every property from that OpenAPI schema. Claude can populate any or all of them.
And read those descriptions again. Both of them start with what the tool does and then say when to call it. That second half is what determines whether the model reaches for your tool at the right moment or ignores it entirely. Write for the model. It is the only reader you have.
Step 6: Build the MCP server
Almost there.
- Click the Servers tab.
- Click Create Server.
- Complete the form as follows:
| Field | Value |
|---|---|
| Label | Incident MCP Server |
| Short Description | Access and manage ServiceNow incidents by incident number. Use this server to retrieve incident details or update incident fields including urgency, state, assignment, and work notes. |
- Click Add Tools.
- Select both Get Incident and Update Incident.
- Click Add
- Click Create.
- Click Activate on the server record. If you see Deactivate instead, it activated on its own and you are fine.
- Copy the Server URL off the record. You need it and the OAuth credentials in the next step.
The console requires at least one tool to exist before you can publish a server, which is why we built tools first. If you tried to start here, that is why it did not work.
Step 7: Connect Claude
- In Claude.ai, click your profile icon.
- Click Settings
- Click Connectors
- Click Add > Add custom connector.
- Complete the form as follows:
| Field | Value |
|---|---|
| Name | ServiceNow Incident MCP |
| Remote MCP Server URL | The Server URL from your MCP Server record |
| OAuth Client ID | From the Claude MCP Connector registry record |
| OAuth Client Secret | From the same record |
- Click Add
- Click Connect to kick off the OAuth flow.
You will land on a consent screen asking whether you want to connect your ServiceNow account to the Claude MCP Connector application, granting the useraccount auth scope. That name should look familiar, because it is the record you created in Episode 4.
Pause on this screen for a second, because it is the moment where trust actually gets established. ServiceNow is not handing Claude a blanket key to your instance. It is asking you, the logged-in user, to explicitly approve this specific application for this specific scope.
If somebody ever asks you what an AI client can and cannot do in your instance, this screen and the application registry record behind it are the answer. It is also the first place to look when something is not behaving the way you expect.
Click Allow.
Step 8: Test it
This is the moment of truth.
- Open a new Claude conversation and confirm the connector is enabled for that chat.
- You need an existing incident number to test against, so grab one or create a test incident and note the number.
Test the read.
Send this prompt, substituting your own incident number:
Get the details of ServiceNow incident INC0009009.
Claude will ask permission to use the Get Incident tool. Allow it. Claude may show an "Always allow" option the first time each tool is invoked, which is a Claude behavior, not a ServiceNow configuration problem.
You should get back the incident details: number, short description, state, urgency, priority, assigned to, and opened at.
Test the write. Same incident number:
Update incident INC0009009. Set urgency to High and add a work note that says "Escalated to network team for investigation."
Claude will ask permission for the Update Incident tool this time. Allow it.
You should get back a confirmation that the incident was updated. Then go open that incident in ServiceNow and check that the urgency and work note actually match what you asked for. The confirmation tells you Claude thinks it worked. The record tells you it did.
Under the hood, each of those two prompts made the same round trip. You asked in plain English. Claude picked a tool and filled in the inputs. The MCP server routed the call to that tool's backing implementation. Your Scripted REST API ran the GlideRecord work, and the answer came back up through each of those same steps in reverse.
If both tools responded, you are done. You built an MCP server on ServiceNow!
Troubleshooting
Connector shows zero tools after connecting.
Check the OAuth token format first. Open the Claude MCP Connector registry record and confirm Token Format is JWT. If it is on the default, Opaque, update it, then disconnect and reconnect in Claude.
Claude connects but tool calls return errors.
Check roles on the account you authenticated with. It needs itil or equivalent read/write access on incident. The useraccount scope delegates the authenticated user's permissions. It does not grant anything that user does not already have. If your login cannot update incidents, neither can Claude.
OAuth flow fails or loops.
Confirm the Redirect URL is exactly https://claude.ai/api/mcp/auth_callback with no trailing slash. If your instance uses SSO, routing through your identity provider first is normal.
Still stuck. Re-run the health check from Episode 1 and confirm you get a healthy response back before you keep digging elsewhere.
More coming
This series is not finished, and this article is going to grow with it.
The rest of the build lands here as I record it, starting with the Scripted REST API that our first MCP tool will call. Subscribe on YouTube if you want the episodes as we post them.
- 3,598 Views
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.