UNPKG

mcp-servicenow

Version:

ServiceNow MCP server for Claude AI integration

429 lines (375 loc) 12.4 kB
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 } };