mcp-servicenow
Version:
ServiceNow MCP server for Claude AI integration
429 lines (375 loc) • 12.4 kB
text/typescript
import axios from 'axios';
import {
ServiceCatalogParams,
GetCatalogItemsParams,
GetRequestsParams,
CatalogItem,
ServiceRequest
} from '../models/service_catalog';
import { log } from '../utils/logger';
// Tool definition for browsing catalog items
export const BrowseCatalogTool = {
name: 'browse_catalog',
description: 'Browse available service catalog items',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Filter by category (e.g., "Hardware", "Software", "Access")'
},
search: {
type: 'string',
description: 'Search term to find specific items'
},
active: {
type: 'boolean',
description: 'Filter by active status (default: true)'
},
limit: {
type: 'number',
description: 'Maximum number of items to return (default: 20)'
}
}
}
};
// Tool definition for requesting catalog items
export const RequestCatalogItemTool = {
name: 'request_catalog_item',
description: 'Request a service catalog item',
inputSchema: {
type: 'object',
properties: {
catalog_item: {
type: 'string',
description: 'Name or sys_id of the catalog item to request'
},
requested_for: {
type: 'string',
description: 'User ID or email for whom the item is requested'
},
short_description: {
type: 'string',
description: 'Brief description of the request'
},
description: {
type: 'string',
description: 'Detailed description or justification'
},
urgency: {
type: 'string',
enum: ['1', '2', '3'],
description: 'Urgency level (1=high, 2=medium, 3=low)'
},
priority: {
type: 'string',
enum: ['1', '2', '3', '4', '5'],
description: 'Priority level (1=critical, 5=planning)'
},
variables: {
type: 'object',
description: 'Catalog item specific variables (e.g., laptop model, software version)'
}
},
required: ['catalog_item']
}
};
// Tool definition for checking request status
export const GetServiceRequestsTool = {
name: 'get_service_requests',
description: 'Get service requests and their status',
inputSchema: {
type: 'object',
properties: {
requested_for: {
type: 'string',
description: 'Filter by user who the request is for'
},
state: {
type: 'string',
description: 'Filter by request state (e.g., "Open", "Work in Progress", "Closed")'
},
created_since: {
type: 'string',
description: 'Filter requests created since this date (YYYY-MM-DD)'
},
limit: {
type: 'number',
description: 'Maximum number of requests to return (default: 50)'
}
}
}
};
// Logic for browsing catalog items
export async function browseCatalog(params: any = {}): Promise<any> {
try {
log(`browseCatalog called with params: ${JSON.stringify(params)}`);
const instanceUrl = process.env.SERVICENOW_INSTANCE_URL;
const username = process.env.SERVICENOW_USERNAME;
const password = process.env.SERVICENOW_PASSWORD;
if (!instanceUrl || !username || !password) {
return {
content: [{
type: 'text',
text: 'Error: Missing ServiceNow credentials. Please check environment variables.'
}]
};
}
// Build query parameters
const queryParts: string[] = [];
if (params.active !== false) {
queryParts.push('active=true');
}
if (params.category) {
queryParts.push(`category.title=${params.category}`);
}
if (params.search) {
queryParts.push(`nameCONTAINS${params.search}^ORshort_descriptionCONTAINS${params.search}`);
}
const requestParams: any = {
sysparm_fields: 'sys_id,name,short_description,description,category.title,price,recurring_price,active',
sysparm_limit: params.limit || 20
};
if (queryParts.length > 0) {
requestParams.sysparm_query = queryParts.join('^');
}
log(`Querying catalog items with: ${JSON.stringify(requestParams)}`);
const response = await axios.get(
`${instanceUrl}/api/now/table/sc_cat_item`,
{
params: requestParams,
auth: { username, password },
headers: { 'Accept': 'application/json' }
}
);
const items = response.data.result;
log(`Found ${items.length} catalog items`);
if (items.length === 0) {
return {
content: [{
type: 'text',
text: 'No catalog items found matching the specified criteria.'
}]
};
}
// Format items for display
const formattedItems = items.map((item: any) => {
const price = item.price ? `$${item.price}` : 'Free';
const recurring = item.recurring_price ? ` (${item.recurring_price} recurring)` : '';
return `• **${item.name}**
Description: ${item.short_description}
Category: ${item.category?.display_value || 'N/A'}
Price: ${price}${recurring}
ID: ${item.sys_id}`;
}).join('\n\n');
return {
content: [{
type: 'text',
text: `Found ${items.length} catalog item(s):\n\n${formattedItems}`
}]
};
} catch (error: any) {
log(`Error browsing catalog: ${error.message}`);
return {
content: [{
type: 'text',
text: `Error browsing catalog: ${error.message}`
}]
};
}
}
// Logic for requesting catalog items
export async function requestCatalogItem(params: any = {}): Promise<any> {
try {
log(`requestCatalogItem called with params: ${JSON.stringify(params)}`);
const instanceUrl = process.env.SERVICENOW_INSTANCE_URL;
const username = process.env.SERVICENOW_USERNAME;
const password = process.env.SERVICENOW_PASSWORD;
const defaultUser = process.env.SERVICENOW_DEFAULT_SENDER_EMAIL;
if (!instanceUrl || !username || !password) {
return {
content: [{
type: 'text',
text: 'Error: Missing ServiceNow credentials.'
}]
};
}
const { catalog_item, requested_for, short_description, description, urgency, priority, variables } = params;
if (!catalog_item) {
return {
content: [{
type: 'text',
text: 'Error: catalog_item parameter is required'
}]
};
}
// First, find the catalog item by name or sys_id
let catalogItemId = catalog_item;
if (!catalog_item.startsWith('sys_id:') && catalog_item.length !== 32) {
// Search by name
const searchResponse = await axios.get(
`${instanceUrl}/api/now/table/sc_cat_item`,
{
params: {
sysparm_query: `name=${catalog_item}^ORshort_descriptionCONTAINS${catalog_item}`,
sysparm_limit: 1,
sysparm_fields: 'sys_id,name'
},
auth: { username, password },
headers: { 'Accept': 'application/json' }
}
);
if (searchResponse.data.result.length === 0) {
return {
content: [{
type: 'text',
text: `Error: Catalog item "${catalog_item}" not found. Please check the item name or use browse_catalog to find available items.`
}]
};
}
catalogItemId = searchResponse.data.result[0].sys_id;
log(`Found catalog item: ${searchResponse.data.result[0].name} (${catalogItemId})`);
}
// Create the service request
const requestData: any = {
catalog_item: catalogItemId,
requested_for: requested_for || defaultUser,
short_description: short_description || `Request for ${catalog_item}`,
description: description || '',
urgency: urgency || '3',
priority: priority || '4'
};
// Add catalog item variables if provided
if (variables && typeof variables === 'object') {
requestData.variables = variables;
}
log(`Creating service request: ${JSON.stringify(requestData)}`);
const response = await axios.post(
`${instanceUrl}/api/now/table/sc_request`,
requestData,
{
auth: { username, password },
headers: { 'Accept': 'application/json' }
}
);
const request = response.data.result;
log(`Service request created successfully: ${request.number}`);
return {
content: [{
type: 'text',
text: `Service request created successfully:
- Request Number: ${request.number}
- System ID: ${request.sys_id}
- State: ${request.state}
- Requested For: ${request.requested_for?.display_value || 'N/A'}
- Created: ${request.sys_created_on}
- Description: ${request.short_description}`
}]
};
} catch (error: any) {
log(`Error creating service request: ${error.message}`);
if (error.response) {
log(`Error response: ${JSON.stringify(error.response.data)}`);
}
return {
content: [{
type: 'text',
text: `Error creating service request: ${error.message}`
}]
};
}
}
// Logic for getting service requests
export async function getServiceRequests(params: any = {}): Promise<any> {
try {
log(`getServiceRequests called with params: ${JSON.stringify(params)}`);
const instanceUrl = process.env.SERVICENOW_INSTANCE_URL;
const username = process.env.SERVICENOW_USERNAME;
const password = process.env.SERVICENOW_PASSWORD;
if (!instanceUrl || !username || !password) {
return {
content: [{
type: 'text',
text: 'Error: Missing ServiceNow credentials.'
}]
};
}
// Build query parameters
const queryParts: string[] = [];
if (params.requested_for) {
queryParts.push(`requested_for.email=${params.requested_for}^ORrequested_for.user_name=${params.requested_for}`);
}
if (params.state) {
queryParts.push(`state=${params.state}`);
}
if (params.created_since) {
queryParts.push(`sys_created_on>=${params.created_since}`);
}
const requestParams: any = {
sysparm_fields: 'number,sys_id,short_description,description,state,requested_for.display_value,requested_by.display_value,sys_created_on,catalog_item.display_value,approval',
sysparm_limit: params.limit || 50,
sysparm_order_by: '-sys_created_on'
};
if (queryParts.length > 0) {
requestParams.sysparm_query = queryParts.join('^');
}
log(`Querying service requests with: ${JSON.stringify(requestParams)}`);
const response = await axios.get(
`${instanceUrl}/api/now/table/sc_request`,
{
params: requestParams,
auth: { username, password },
headers: { 'Accept': 'application/json' }
}
);
const requests = response.data.result;
log(`Found ${requests.length} service requests`);
if (requests.length === 0) {
return {
content: [{
type: 'text',
text: 'No service requests found matching the specified criteria.'
}]
};
}
// Format requests for display
const formattedRequests = requests.map((request: any) => {
return `• **${request.number}** - ${request.short_description}
State: ${request.state}
Catalog Item: ${request.catalog_item?.display_value || 'N/A'}
Requested For: ${request.requested_for?.display_value || 'N/A'}
Requested By: ${request.requested_by?.display_value || 'N/A'}
Created: ${request.sys_created_on}
Approval: ${request.approval || 'Not Required'}`;
}).join('\n\n');
return {
content: [{
type: 'text',
text: `Found ${requests.length} service request(s):\n\n${formattedRequests}`
}]
};
} catch (error: any) {
log(`Error getting service requests: ${error.message}`);
return {
content: [{
type: 'text',
text: `Error getting service requests: ${error.message}`
}]
};
}
}
// Collection of all service catalog tools
export const catalogTools = {
[BrowseCatalogTool.name]: {
definition: BrowseCatalogTool,
execute: browseCatalog
},
[RequestCatalogItemTool.name]: {
definition: RequestCatalogItemTool,
execute: requestCatalogItem
},
[GetServiceRequestsTool.name]: {
definition: GetServiceRequestsTool,
execute: getServiceRequests
}
};