@mindstone-engineering/mcp-server-servicenow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ServiceNow authentication module.
3
+ *
4
+ * Manages instance name + username/password lifecycle — env var on startup,
5
+ * runtime update via configure tool, and bridge integration for host-app
6
+ * credential management.
7
+ *
8
+ * Uses Basic auth: Authorization: Basic base64(username:password)
9
+ */
10
+ /**
11
+ * Normalizes a user-provided ServiceNow instance input to just the subdomain label.
12
+ * Accepts: "acme", "acme.service-now.com", "https://acme.service-now.com", etc.
13
+ * Returns undefined if the input is invalid.
14
+ */
15
+ export declare function normalizeServiceNowInstanceInput(input: string | undefined): string | undefined;
16
+ /**
17
+ * Returns the current ServiceNow instance name (subdomain label).
18
+ */
19
+ export declare function getInstance(): string;
20
+ /**
21
+ * Returns the current ServiceNow username.
22
+ */
23
+ export declare function getUsername(): string;
24
+ /**
25
+ * Returns the current ServiceNow password.
26
+ */
27
+ export declare function getPassword(): string;
28
+ /**
29
+ * Returns true if all three credentials are configured.
30
+ */
31
+ export declare function isConfigured(): boolean;
32
+ /**
33
+ * Update credentials at runtime (e.g. after configure_servicenow).
34
+ */
35
+ export declare function setCredentials(inst: string, user: string, pass: string): void;
36
+ //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * ServiceNow authentication module.
3
+ *
4
+ * Manages instance name + username/password lifecycle — env var on startup,
5
+ * runtime update via configure tool, and bridge integration for host-app
6
+ * credential management.
7
+ *
8
+ * Uses Basic auth: Authorization: Basic base64(username:password)
9
+ */
10
+ import { SINGLE_LABEL_INSTANCE_REGEX } from './types.js';
11
+ let instance = '';
12
+ let username = process.env.SERVICENOW_USERNAME || '';
13
+ let password = process.env.SERVICENOW_PASSWORD || '';
14
+ /**
15
+ * Extracts a hostname from a user-provided input string.
16
+ * Handles URLs, bare hostnames, and various formats.
17
+ */
18
+ function extractHostnameFromUserInput(input) {
19
+ const trimmed = input.trim();
20
+ if (!trimmed)
21
+ return '';
22
+ const candidate = trimmed.includes('://') ? trimmed : `https://${trimmed}`;
23
+ try {
24
+ return new URL(candidate).hostname.toLowerCase();
25
+ }
26
+ catch {
27
+ return trimmed
28
+ .toLowerCase()
29
+ .replace(/^[a-z]+:\/\//, '')
30
+ .split('/')[0]
31
+ .split('?')[0]
32
+ .split('#')[0]
33
+ .split(':')[0];
34
+ }
35
+ }
36
+ /**
37
+ * Normalizes a user-provided ServiceNow instance input to just the subdomain label.
38
+ * Accepts: "acme", "acme.service-now.com", "https://acme.service-now.com", etc.
39
+ * Returns undefined if the input is invalid.
40
+ */
41
+ export function normalizeServiceNowInstanceInput(input) {
42
+ if (!input)
43
+ return undefined;
44
+ const hostname = extractHostnameFromUserInput(input);
45
+ if (!hostname)
46
+ return undefined;
47
+ const normalizedHostname = hostname.trim().toLowerCase().replace(/\.$/, '');
48
+ const withoutSuffix = normalizedHostname.endsWith('.service-now.com')
49
+ ? normalizedHostname.slice(0, -'.service-now.com'.length)
50
+ : normalizedHostname;
51
+ if (!withoutSuffix || withoutSuffix.includes('.') || !SINGLE_LABEL_INSTANCE_REGEX.test(withoutSuffix)) {
52
+ return undefined;
53
+ }
54
+ return withoutSuffix;
55
+ }
56
+ // Initialize instance from env on module load
57
+ instance = normalizeServiceNowInstanceInput(process.env.SERVICENOW_INSTANCE) || '';
58
+ /**
59
+ * Returns the current ServiceNow instance name (subdomain label).
60
+ */
61
+ export function getInstance() {
62
+ return instance;
63
+ }
64
+ /**
65
+ * Returns the current ServiceNow username.
66
+ */
67
+ export function getUsername() {
68
+ return username;
69
+ }
70
+ /**
71
+ * Returns the current ServiceNow password.
72
+ */
73
+ export function getPassword() {
74
+ return password;
75
+ }
76
+ /**
77
+ * Returns true if all three credentials are configured.
78
+ */
79
+ export function isConfigured() {
80
+ return instance.length > 0 && username.length > 0 && password.length > 0;
81
+ }
82
+ /**
83
+ * Update credentials at runtime (e.g. after configure_servicenow).
84
+ */
85
+ export function setCredentials(inst, user, pass) {
86
+ instance = inst;
87
+ username = user;
88
+ password = pass;
89
+ }
90
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Path to bridge state file, supporting both current and legacy env vars.
3
+ */
4
+ export declare const BRIDGE_STATE_PATH: string;
5
+ /**
6
+ * Send a request to the host app bridge.
7
+ *
8
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
9
+ * that handles credential management and other cross-process operations.
10
+ */
11
+ export declare const bridgeRequest: (urlPath: string, body: Record<string, unknown>) => Promise<{
12
+ success: boolean;
13
+ warning?: string;
14
+ error?: string;
15
+ }>;
16
+ //# sourceMappingURL=bridge.d.ts.map
package/dist/bridge.js ADDED
@@ -0,0 +1,43 @@
1
+ import * as fs from 'fs';
2
+ import { REQUEST_TIMEOUT_MS } from './types.js';
3
+ /**
4
+ * Path to bridge state file, supporting both current and legacy env vars.
5
+ */
6
+ export const BRIDGE_STATE_PATH = process.env.MCP_HOST_BRIDGE_STATE || process.env.MINDSTONE_REBEL_BRIDGE_STATE || '';
7
+ const loadBridgeState = () => {
8
+ if (!BRIDGE_STATE_PATH)
9
+ return null;
10
+ try {
11
+ const raw = fs.readFileSync(BRIDGE_STATE_PATH, 'utf8');
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ };
18
+ /**
19
+ * Send a request to the host app bridge.
20
+ *
21
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
22
+ * that handles credential management and other cross-process operations.
23
+ */
24
+ export const bridgeRequest = async (urlPath, body) => {
25
+ const bridge = loadBridgeState();
26
+ if (!bridge) {
27
+ return { success: false, error: 'Bridge not available' };
28
+ }
29
+ const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
30
+ method: 'POST',
31
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Authorization: `Bearer ${bridge.token}`,
35
+ },
36
+ body: JSON.stringify(body),
37
+ });
38
+ if (response.status === 401 || response.status === 403) {
39
+ return { success: false, error: `Bridge returned ${response.status}: unauthorized. Check host app authentication.` };
40
+ }
41
+ return response.json();
42
+ };
43
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ServiceNow API HTTP client.
3
+ *
4
+ * Centralises Basic auth header injection, error handling, rate-limit
5
+ * messaging, and instance URL construction for all ServiceNow API calls.
6
+ *
7
+ * Auth: Authorization: Basic base64(username:password)
8
+ * Base URL: https://{instance}.service-now.com/api/now/table
9
+ */
10
+ /**
11
+ * Builds a query string from key-value pairs, omitting undefined/empty values.
12
+ */
13
+ export declare function buildQueryParams(params: Record<string, string | number | undefined>): string;
14
+ /**
15
+ * Make an authenticated request to the ServiceNow Table API.
16
+ *
17
+ * @param tablePath Path relative to /api/now/table, e.g. `/incident`
18
+ * @param options Additional fetch options (method, body, headers)
19
+ * @returns Parsed JSON result from the ServiceNow response
20
+ */
21
+ export declare function servicenowFetch<T>(tablePath: string, options?: RequestInit): Promise<T>;
22
+ //# sourceMappingURL=client.d.ts.map
package/dist/client.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * ServiceNow API HTTP client.
3
+ *
4
+ * Centralises Basic auth header injection, error handling, rate-limit
5
+ * messaging, and instance URL construction for all ServiceNow API calls.
6
+ *
7
+ * Auth: Authorization: Basic base64(username:password)
8
+ * Base URL: https://{instance}.service-now.com/api/now/table
9
+ */
10
+ import { getInstance, getUsername, getPassword, isConfigured } from './auth.js';
11
+ import { ServiceNowError, REQUEST_TIMEOUT_MS } from './types.js';
12
+ /**
13
+ * Returns the ServiceNow Table API base URL for the current instance.
14
+ */
15
+ function getBaseUrl() {
16
+ return `https://${getInstance()}.service-now.com/api/now/table`;
17
+ }
18
+ /**
19
+ * Builds a query string from key-value pairs, omitting undefined/empty values.
20
+ */
21
+ export function buildQueryParams(params) {
22
+ const entries = Object.entries(params)
23
+ .filter(([, v]) => v !== undefined && v !== '')
24
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
25
+ return entries.length > 0 ? `?${entries.join('&')}` : '';
26
+ }
27
+ /**
28
+ * Make an authenticated request to the ServiceNow Table API.
29
+ *
30
+ * @param tablePath Path relative to /api/now/table, e.g. `/incident`
31
+ * @param options Additional fetch options (method, body, headers)
32
+ * @returns Parsed JSON result from the ServiceNow response
33
+ */
34
+ export async function servicenowFetch(tablePath, options = {}) {
35
+ if (!isConfigured()) {
36
+ throw new ServiceNowError('ServiceNow not configured', 'AUTH_REQUIRED', 'Use configure_servicenow to set your instance name, username, and password first.');
37
+ }
38
+ const url = `${getBaseUrl()}${tablePath}`;
39
+ const authHeader = 'Basic ' + Buffer.from(`${getUsername()}:${getPassword()}`).toString('base64');
40
+ let response;
41
+ try {
42
+ response = await fetch(url, {
43
+ ...options,
44
+ signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
45
+ headers: {
46
+ Authorization: authHeader,
47
+ Accept: 'application/json',
48
+ 'Content-Type': 'application/json',
49
+ ...options.headers,
50
+ },
51
+ });
52
+ }
53
+ catch (error) {
54
+ if (error instanceof Error && error.name === 'TimeoutError') {
55
+ throw new ServiceNowError('Request to ServiceNow API timed out', 'TIMEOUT', 'The request took too long. Try again or check if the ServiceNow instance is available.');
56
+ }
57
+ throw error;
58
+ }
59
+ if (response.status === 401 || response.status === 403) {
60
+ throw new ServiceNowError('Authentication failed. Check your instance name, username, and password.', 'AUTH_FAILED', 'Re-configure with configure_servicenow. Ensure the account has itil and knowledge roles.');
61
+ }
62
+ if (response.status === 429) {
63
+ throw new ServiceNowError('Rate limited by ServiceNow. Please wait before retrying.', 'RATE_LIMITED', 'Please wait before retrying. ServiceNow has rate limits based on your instance configuration.');
64
+ }
65
+ if (response.status === 404) {
66
+ throw new ServiceNowError('Resource not found', 'NOT_FOUND', 'The requested record does not exist. Verify the identifier.');
67
+ }
68
+ // Check for non-JSON responses (hibernating instance, login page, etc.)
69
+ const contentType = (response.headers.get('content-type') || '').toLowerCase();
70
+ if (!response.ok) {
71
+ let errorText;
72
+ try {
73
+ const errorBody = (await response.json());
74
+ errorText =
75
+ errorBody?.error?.message || errorBody?.error?.detail || JSON.stringify(errorBody);
76
+ }
77
+ catch {
78
+ errorText = await response.text().catch(() => 'Unknown error');
79
+ }
80
+ throw new ServiceNowError(`ServiceNow API error (${response.status}): ${errorText}`, 'API_ERROR', 'Check the request parameters and try again.');
81
+ }
82
+ if (!contentType.includes('application/json')) {
83
+ const bodyPreview = await response.text().catch(() => '(could not read body)');
84
+ const isHibernating = bodyPreview.toLowerCase().includes('hibernat');
85
+ throw new ServiceNowError(isHibernating
86
+ ? `ServiceNow instance '${getInstance()}' is hibernating. Wake it at https://developer.servicenow.com and try again in a few minutes.`
87
+ : `ServiceNow returned non-JSON response (Content-Type: ${contentType}). The instance may be down, misconfigured, or returning a login page.`, isHibernating ? 'INSTANCE_HIBERNATING' : 'UNEXPECTED_CONTENT_TYPE', isHibernating
88
+ ? 'Wake the instance at https://developer.servicenow.com, wait a few minutes, then try again.'
89
+ : 'Check that the instance name is correct and the instance is active.');
90
+ }
91
+ const body = (await response.json());
92
+ return body.result;
93
+ }
94
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ServiceNow MCP Server
4
+ *
5
+ * Provides ServiceNow ITSM integration via Model Context Protocol.
6
+ * Covers incidents, change requests, knowledge base articles, and users.
7
+ *
8
+ * Environment variables:
9
+ * - SERVICENOW_INSTANCE: ServiceNow instance name (e.g., "acme" for acme.service-now.com)
10
+ * - SERVICENOW_USERNAME: ServiceNow username
11
+ * - SERVICENOW_PASSWORD: ServiceNow password
12
+ * - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
13
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ServiceNow MCP Server
4
+ *
5
+ * Provides ServiceNow ITSM integration via Model Context Protocol.
6
+ * Covers incidents, change requests, knowledge base articles, and users.
7
+ *
8
+ * Environment variables:
9
+ * - SERVICENOW_INSTANCE: ServiceNow instance name (e.g., "acme" for acme.service-now.com)
10
+ * - SERVICENOW_USERNAME: ServiceNow username
11
+ * - SERVICENOW_PASSWORD: ServiceNow password
12
+ * - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
13
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
14
+ */
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import { createServer } from './server.js';
17
+ async function main() {
18
+ const server = createServer();
19
+ const transport = new StdioServerTransport();
20
+ await server.connect(transport);
21
+ console.error('ServiceNow MCP server running on stdio');
22
+ }
23
+ main().catch((error) => {
24
+ console.error('Fatal error:', error);
25
+ process.exit(1);
26
+ });
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
3
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,15 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerConfigureTools, registerIncidentTools, registerChangeTools, registerKnowledgeTools, registerUserTools, } from './tools/index.js';
3
+ export function createServer() {
4
+ const server = new McpServer({
5
+ name: 'servicenow-mcp-server',
6
+ version: '0.1.0',
7
+ });
8
+ registerConfigureTools(server);
9
+ registerIncidentTools(server);
10
+ registerChangeTools(server);
11
+ registerKnowledgeTools(server);
12
+ registerUserTools(server);
13
+ return server;
14
+ }
15
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerChangeTools(server: McpServer): void;
3
+ //# sourceMappingURL=changes.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ import { servicenowFetch, buildQueryParams } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ export function registerChangeTools(server) {
5
+ // ── list_servicenow_change_requests ───────────────────────────
6
+ server.registerTool('list_servicenow_change_requests', {
7
+ description: 'List or search change requests in ServiceNow. ' +
8
+ 'Returns: number, short_description, state, type, priority, assigned_to, start_date, end_date. ' +
9
+ 'Use ServiceNow encoded query syntax for filtering. ' +
10
+ 'Type values: "normal", "standard", "emergency".',
11
+ inputSchema: z.object({
12
+ query: z
13
+ .string()
14
+ .optional()
15
+ .describe('ServiceNow encoded query (e.g., "state=implement^type=normal")'),
16
+ limit: z
17
+ .number()
18
+ .optional()
19
+ .default(20)
20
+ .describe('Max results to return (default: 20)'),
21
+ offset: z
22
+ .number()
23
+ .optional()
24
+ .default(0)
25
+ .describe('Offset for pagination (default: 0)'),
26
+ }),
27
+ annotations: { readOnlyHint: true },
28
+ }, withErrorHandling(async (args) => {
29
+ const params = buildQueryParams({
30
+ sysparm_limit: args.limit ?? 20,
31
+ sysparm_offset: args.offset ?? 0,
32
+ sysparm_display_value: 'true',
33
+ sysparm_fields: 'number,short_description,state,type,priority,assigned_to,start_date,end_date',
34
+ sysparm_query: args.query,
35
+ });
36
+ const changeRequests = await servicenowFetch(`/change_request${params}`);
37
+ return JSON.stringify({
38
+ ok: true,
39
+ change_requests: changeRequests,
40
+ count: changeRequests.length,
41
+ });
42
+ }));
43
+ // ── get_servicenow_change_request ─────────────────────────────
44
+ server.registerTool('get_servicenow_change_request', {
45
+ description: 'Get a single change request by number (e.g., CHG0010001) or sys_id. ' +
46
+ 'Returns the full change request record with all fields.',
47
+ inputSchema: z.object({
48
+ identifier: z
49
+ .string()
50
+ .min(1)
51
+ .describe('Change request number (e.g., CHG0010001) or sys_id'),
52
+ }),
53
+ annotations: { readOnlyHint: true },
54
+ }, withErrorHandling(async (args) => {
55
+ if (args.identifier.toUpperCase().startsWith('CHG')) {
56
+ const params = buildQueryParams({
57
+ sysparm_query: `number=${args.identifier}`,
58
+ sysparm_limit: 1,
59
+ sysparm_display_value: 'true',
60
+ });
61
+ const results = await servicenowFetch(`/change_request${params}`);
62
+ if (results.length === 0) {
63
+ return JSON.stringify({
64
+ ok: false,
65
+ error: `Change request ${args.identifier} not found.`,
66
+ });
67
+ }
68
+ return JSON.stringify({ ok: true, change_request: results[0] });
69
+ }
70
+ const changeRequest = await servicenowFetch(`/change_request/${encodeURIComponent(args.identifier)}?sysparm_display_value=true`);
71
+ return JSON.stringify({ ok: true, change_request: changeRequest });
72
+ }));
73
+ }
74
+ //# sourceMappingURL=changes.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerConfigureTools(server: McpServer): void;
3
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+ import { normalizeServiceNowInstanceInput, setCredentials } from '../auth.js';
3
+ import { bridgeRequest, BRIDGE_STATE_PATH } from '../bridge.js';
4
+ import { ServiceNowError } from '../types.js';
5
+ import { withErrorHandling } from '../utils.js';
6
+ export function registerConfigureTools(server) {
7
+ server.registerTool('configure_servicenow', {
8
+ description: 'Configure ServiceNow credentials. Call this when the user provides their instance name, username, and password. ' +
9
+ 'The instance name is the subdomain part of the URL (e.g., "acme" for acme.service-now.com). ' +
10
+ 'You can also paste the full URL. The account needs read/write access to incident, change_request, kb_knowledge, and sys_user tables. ' +
11
+ 'For production use, consider creating a dedicated integration user with appropriate roles (itil, knowledge).',
12
+ inputSchema: z.object({
13
+ instance: z
14
+ .string()
15
+ .min(1)
16
+ .describe('ServiceNow instance name (e.g., "acme") or full URL (e.g., "https://acme.service-now.com")'),
17
+ username: z.string().min(1).describe('ServiceNow username'),
18
+ password: z.string().min(1).describe('ServiceNow password'),
19
+ }),
20
+ annotations: { readOnlyHint: false, destructiveHint: false },
21
+ }, withErrorHandling(async (args) => {
22
+ const normalized = normalizeServiceNowInstanceInput(args.instance);
23
+ const trimmedUsername = args.username.trim();
24
+ const trimmedPassword = args.password.trim();
25
+ if (!normalized) {
26
+ return JSON.stringify({
27
+ ok: false,
28
+ error: 'Invalid instance. Enter just the instance name (e.g., "acme"), or paste your ServiceNow URL (e.g., https://acme.service-now.com).',
29
+ });
30
+ }
31
+ if (!trimmedUsername || !trimmedPassword) {
32
+ return JSON.stringify({
33
+ ok: false,
34
+ error: 'instance, username, and password are all required.',
35
+ });
36
+ }
37
+ // If bridge is available, persist via bridge
38
+ if (BRIDGE_STATE_PATH) {
39
+ try {
40
+ const result = await bridgeRequest('/bundled/servicenow/configure', {
41
+ instance: normalized,
42
+ username: trimmedUsername,
43
+ password: trimmedPassword,
44
+ });
45
+ if (result.success) {
46
+ setCredentials(normalized, trimmedUsername, trimmedPassword);
47
+ const message = result.warning
48
+ ? `ServiceNow configured successfully for ${normalized}.service-now.com. Note: ${result.warning}`
49
+ : `ServiceNow configured successfully for ${normalized}.service-now.com! Try list_servicenow_incidents or search_servicenow_knowledge.`;
50
+ return JSON.stringify({ ok: true, message });
51
+ }
52
+ // Bridge returned failure — surface as error, do NOT fall through
53
+ throw new ServiceNowError(result.error || 'Bridge configuration failed', 'BRIDGE_ERROR', 'The host app bridge rejected the configuration request. Check the host app logs.');
54
+ }
55
+ catch (error) {
56
+ if (error instanceof ServiceNowError)
57
+ throw error;
58
+ // Bridge request failed (network, timeout, etc.) — surface as error
59
+ throw new ServiceNowError(`Bridge request failed: ${error instanceof Error ? error.message : String(error)}`, 'BRIDGE_ERROR', 'Could not reach the host app bridge. Ensure the host app is running.');
60
+ }
61
+ }
62
+ // No bridge configured — configure in-memory only
63
+ setCredentials(normalized, trimmedUsername, trimmedPassword);
64
+ return JSON.stringify({
65
+ ok: true,
66
+ message: `ServiceNow configured successfully for ${normalized}.service-now.com! Try list_servicenow_incidents or search_servicenow_knowledge.`,
67
+ });
68
+ }));
69
+ }
70
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerIncidentTools(server: McpServer): void;
3
+ //# sourceMappingURL=incidents.d.ts.map
@@ -0,0 +1,177 @@
1
+ import { z } from 'zod';
2
+ import { servicenowFetch, buildQueryParams } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ export function registerIncidentTools(server) {
5
+ // ── list_servicenow_incidents ─────────────────────────────────
6
+ server.registerTool('list_servicenow_incidents', {
7
+ description: 'List or search incidents in ServiceNow. ' +
8
+ 'Returns: number, short_description, state, priority, assigned_to, sys_created_on, sys_updated_on, urgency, impact. ' +
9
+ 'Use ServiceNow encoded query syntax for filtering (^ as AND separator). ' +
10
+ 'Use get_servicenow_incident for full details of a specific incident.',
11
+ inputSchema: z.object({
12
+ query: z
13
+ .string()
14
+ .optional()
15
+ .describe('ServiceNow encoded query (e.g., "active=true^priority=1")'),
16
+ limit: z
17
+ .number()
18
+ .optional()
19
+ .default(20)
20
+ .describe('Max results to return (default: 20)'),
21
+ offset: z
22
+ .number()
23
+ .optional()
24
+ .default(0)
25
+ .describe('Offset for pagination (default: 0)'),
26
+ }),
27
+ annotations: { readOnlyHint: true },
28
+ }, withErrorHandling(async (args) => {
29
+ const params = buildQueryParams({
30
+ sysparm_limit: args.limit ?? 20,
31
+ sysparm_offset: args.offset ?? 0,
32
+ sysparm_display_value: 'true',
33
+ sysparm_fields: 'number,short_description,state,priority,assigned_to,sys_created_on,sys_updated_on,urgency,impact',
34
+ sysparm_query: args.query,
35
+ });
36
+ const incidents = await servicenowFetch(`/incident${params}`);
37
+ return JSON.stringify({ ok: true, incidents, count: incidents.length });
38
+ }));
39
+ // ── get_servicenow_incident ───────────────────────────────────
40
+ server.registerTool('get_servicenow_incident', {
41
+ description: 'Get a single incident by number (e.g., INC0010001) or sys_id. ' +
42
+ 'Returns the full incident record with all fields.',
43
+ inputSchema: z.object({
44
+ identifier: z
45
+ .string()
46
+ .min(1)
47
+ .describe('Incident number (e.g., INC0010001) or sys_id'),
48
+ }),
49
+ annotations: { readOnlyHint: true },
50
+ }, withErrorHandling(async (args) => {
51
+ if (args.identifier.toUpperCase().startsWith('INC')) {
52
+ const params = buildQueryParams({
53
+ sysparm_query: `number=${args.identifier}`,
54
+ sysparm_limit: 1,
55
+ sysparm_display_value: 'true',
56
+ });
57
+ const results = await servicenowFetch(`/incident${params}`);
58
+ if (results.length === 0) {
59
+ return JSON.stringify({
60
+ ok: false,
61
+ error: `Incident ${args.identifier} not found.`,
62
+ });
63
+ }
64
+ return JSON.stringify({ ok: true, incident: results[0] });
65
+ }
66
+ // Treat as sys_id
67
+ const incident = await servicenowFetch(`/incident/${encodeURIComponent(args.identifier)}?sysparm_display_value=true`);
68
+ return JSON.stringify({ ok: true, incident });
69
+ }));
70
+ // ── create_servicenow_incident ────────────────────────────────
71
+ server.registerTool('create_servicenow_incident', {
72
+ description: 'Create a new incident in ServiceNow. ' +
73
+ 'Provide at minimum a short_description. ' +
74
+ 'Set urgency and impact to control priority (1=High, 2=Medium, 3=Low). ' +
75
+ 'Note: urgency and impact are strings "1", "2", "3".',
76
+ inputSchema: z.object({
77
+ short_description: z.string().min(1).describe('Brief description of the incident'),
78
+ description: z.string().optional().describe('Detailed description'),
79
+ urgency: z
80
+ .string()
81
+ .optional()
82
+ .describe('Urgency: "1" (High), "2" (Medium), "3" (Low)'),
83
+ impact: z
84
+ .string()
85
+ .optional()
86
+ .describe('Impact: "1" (High), "2" (Medium), "3" (Low)'),
87
+ assignment_group: z.string().optional().describe('Assignment group name'),
88
+ caller_id: z.string().optional().describe('Caller user name or sys_id'),
89
+ category: z.string().optional().describe('Incident category'),
90
+ }),
91
+ annotations: { readOnlyHint: false, destructiveHint: false },
92
+ }, withErrorHandling(async (args) => {
93
+ const body = {};
94
+ if (args.short_description)
95
+ body.short_description = args.short_description;
96
+ if (args.description)
97
+ body.description = args.description;
98
+ if (args.urgency)
99
+ body.urgency = args.urgency;
100
+ if (args.impact)
101
+ body.impact = args.impact;
102
+ if (args.assignment_group)
103
+ body.assignment_group = args.assignment_group;
104
+ if (args.caller_id)
105
+ body.caller_id = args.caller_id;
106
+ if (args.category)
107
+ body.category = args.category;
108
+ const params = buildQueryParams({ sysparm_display_value: 'true' });
109
+ const incident = await servicenowFetch(`/incident${params}`, {
110
+ method: 'POST',
111
+ body: JSON.stringify(body),
112
+ });
113
+ return JSON.stringify({ ok: true, message: 'Incident created.', incident });
114
+ }));
115
+ // ── update_servicenow_incident ────────────────────────────────
116
+ server.registerTool('update_servicenow_incident', {
117
+ description: 'Update an existing incident in ServiceNow by sys_id. ' +
118
+ 'Use get_servicenow_incident to find the sys_id first. ' +
119
+ 'State values: "1" (New), "2" (In Progress), "3" (On Hold), "6" (Resolved), "7" (Closed).',
120
+ inputSchema: z.object({
121
+ sys_id: z
122
+ .string()
123
+ .min(1)
124
+ .describe('Incident sys_id (use get_servicenow_incident to find it)'),
125
+ short_description: z.string().optional().describe('Brief description'),
126
+ description: z.string().optional().describe('Detailed description'),
127
+ state: z
128
+ .string()
129
+ .optional()
130
+ .describe('State: "1" (New), "2" (In Progress), "3" (On Hold), "6" (Resolved), "7" (Closed)'),
131
+ urgency: z
132
+ .string()
133
+ .optional()
134
+ .describe('Urgency: "1" (High), "2" (Medium), "3" (Low)'),
135
+ impact: z
136
+ .string()
137
+ .optional()
138
+ .describe('Impact: "1" (High), "2" (Medium), "3" (Low)'),
139
+ assigned_to: z.string().optional().describe('Assigned to user name or sys_id'),
140
+ assignment_group: z.string().optional().describe('Assignment group name'),
141
+ close_code: z
142
+ .string()
143
+ .optional()
144
+ .describe('Close code (required when resolving)'),
145
+ close_notes: z
146
+ .string()
147
+ .optional()
148
+ .describe('Close notes (required when resolving)'),
149
+ }),
150
+ annotations: { readOnlyHint: false, destructiveHint: false },
151
+ }, withErrorHandling(async (args) => {
152
+ const body = {};
153
+ const updatableFields = [
154
+ 'short_description',
155
+ 'description',
156
+ 'state',
157
+ 'urgency',
158
+ 'impact',
159
+ 'assigned_to',
160
+ 'assignment_group',
161
+ 'close_code',
162
+ 'close_notes',
163
+ ];
164
+ for (const field of updatableFields) {
165
+ if (args[field] !== undefined) {
166
+ body[field] = args[field];
167
+ }
168
+ }
169
+ const params = buildQueryParams({ sysparm_display_value: 'true' });
170
+ const incident = await servicenowFetch(`/incident/${encodeURIComponent(args.sys_id)}${params}`, {
171
+ method: 'PATCH',
172
+ body: JSON.stringify(body),
173
+ });
174
+ return JSON.stringify({ ok: true, message: 'Incident updated.', incident });
175
+ }));
176
+ }
177
+ //# sourceMappingURL=incidents.js.map
@@ -0,0 +1,6 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerIncidentTools } from './incidents.js';
3
+ export { registerChangeTools } from './changes.js';
4
+ export { registerKnowledgeTools } from './knowledge.js';
5
+ export { registerUserTools } from './users.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerIncidentTools } from './incidents.js';
3
+ export { registerChangeTools } from './changes.js';
4
+ export { registerKnowledgeTools } from './knowledge.js';
5
+ export { registerUserTools } from './users.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerKnowledgeTools(server: McpServer): void;
3
+ //# sourceMappingURL=knowledge.d.ts.map
@@ -0,0 +1,75 @@
1
+ import { z } from 'zod';
2
+ import { servicenowFetch, buildQueryParams } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ export function registerKnowledgeTools(server) {
5
+ // ── search_servicenow_knowledge ───────────────────────────────
6
+ server.registerTool('search_servicenow_knowledge', {
7
+ description: 'Search knowledge base articles in ServiceNow. ' +
8
+ 'Returns: number, short_description, sys_created_on, author, kb_knowledge_base, workflow_state. ' +
9
+ 'Simple text queries are automatically converted to LIKE queries. ' +
10
+ 'For advanced filtering, use ServiceNow encoded query syntax directly.',
11
+ inputSchema: z.object({
12
+ query: z
13
+ .string()
14
+ .optional()
15
+ .describe('Search keywords or ServiceNow encoded query'),
16
+ limit: z
17
+ .number()
18
+ .optional()
19
+ .default(20)
20
+ .describe('Max results to return (default: 20)'),
21
+ offset: z
22
+ .number()
23
+ .optional()
24
+ .default(0)
25
+ .describe('Offset for pagination (default: 0)'),
26
+ }),
27
+ annotations: { readOnlyHint: true },
28
+ }, withErrorHandling(async (args) => {
29
+ let query = args.query;
30
+ // If the query doesn't look like an encoded query, treat it as a keyword search
31
+ if (query && !query.includes('=') && !query.includes('^')) {
32
+ query = `short_descriptionLIKE${query}^ORtextLIKE${query}`;
33
+ }
34
+ const params = buildQueryParams({
35
+ sysparm_limit: args.limit ?? 20,
36
+ sysparm_offset: args.offset ?? 0,
37
+ sysparm_display_value: 'true',
38
+ sysparm_fields: 'number,short_description,sys_created_on,author,kb_knowledge_base,workflow_state',
39
+ sysparm_query: query,
40
+ });
41
+ const articles = await servicenowFetch(`/kb_knowledge${params}`);
42
+ return JSON.stringify({ ok: true, articles, count: articles.length });
43
+ }));
44
+ // ── get_servicenow_knowledge_article ──────────────────────────
45
+ server.registerTool('get_servicenow_knowledge_article', {
46
+ description: 'Get a full knowledge base article by sys_id or number (e.g., KB0010001). ' +
47
+ 'Returns the full article record including the article body text.',
48
+ inputSchema: z.object({
49
+ identifier: z
50
+ .string()
51
+ .min(1)
52
+ .describe('KB article number (e.g., KB0010001) or sys_id'),
53
+ }),
54
+ annotations: { readOnlyHint: true },
55
+ }, withErrorHandling(async (args) => {
56
+ if (args.identifier.toUpperCase().startsWith('KB')) {
57
+ const params = buildQueryParams({
58
+ sysparm_query: `number=${args.identifier}`,
59
+ sysparm_limit: 1,
60
+ sysparm_display_value: 'true',
61
+ });
62
+ const results = await servicenowFetch(`/kb_knowledge${params}`);
63
+ if (results.length === 0) {
64
+ return JSON.stringify({
65
+ ok: false,
66
+ error: `Knowledge article ${args.identifier} not found.`,
67
+ });
68
+ }
69
+ return JSON.stringify({ ok: true, article: results[0] });
70
+ }
71
+ const article = await servicenowFetch(`/kb_knowledge/${encodeURIComponent(args.identifier)}?sysparm_display_value=true`);
72
+ return JSON.stringify({ ok: true, article });
73
+ }));
74
+ }
75
+ //# sourceMappingURL=knowledge.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerUserTools(server: McpServer): void;
3
+ //# sourceMappingURL=users.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ import { servicenowFetch, buildQueryParams } from '../client.js';
3
+ import { withErrorHandling } from '../utils.js';
4
+ export function registerUserTools(server) {
5
+ // ── list_servicenow_users ─────────────────────────────────────
6
+ server.registerTool('list_servicenow_users', {
7
+ description: 'List or search users in ServiceNow. ' +
8
+ 'Returns: sys_id, user_name, first_name, last_name, email, title, department, active. ' +
9
+ 'user_name is the login username, not the display name. ' +
10
+ 'For name searches, use first_nameLIKE or last_nameLIKE in the query.',
11
+ inputSchema: z.object({
12
+ query: z
13
+ .string()
14
+ .optional()
15
+ .describe('ServiceNow encoded query (e.g., "active=true^departmentLIKEengineering")'),
16
+ limit: z
17
+ .number()
18
+ .optional()
19
+ .default(20)
20
+ .describe('Max results to return (default: 20)'),
21
+ offset: z
22
+ .number()
23
+ .optional()
24
+ .default(0)
25
+ .describe('Offset for pagination (default: 0)'),
26
+ }),
27
+ annotations: { readOnlyHint: true },
28
+ }, withErrorHandling(async (args) => {
29
+ const params = buildQueryParams({
30
+ sysparm_limit: args.limit ?? 20,
31
+ sysparm_offset: args.offset ?? 0,
32
+ sysparm_display_value: 'true',
33
+ sysparm_fields: 'sys_id,user_name,first_name,last_name,email,title,department,active',
34
+ sysparm_query: args.query,
35
+ });
36
+ const users = await servicenowFetch(`/sys_user${params}`);
37
+ return JSON.stringify({ ok: true, users, count: users.length });
38
+ }));
39
+ }
40
+ //# sourceMappingURL=users.js.map
@@ -0,0 +1,16 @@
1
+ export declare const REQUEST_TIMEOUT_MS = 30000;
2
+ export interface BridgeState {
3
+ port: number;
4
+ token: string;
5
+ }
6
+ export declare class ServiceNowError extends Error {
7
+ readonly code: string;
8
+ readonly resolution: string;
9
+ constructor(message: string, code: string, resolution: string);
10
+ }
11
+ /**
12
+ * Regex for a valid single-label ServiceNow instance name:
13
+ * lowercase alphanumeric with optional hyphens between segments.
14
+ */
15
+ export declare const SINGLE_LABEL_INSTANCE_REGEX: RegExp;
16
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,17 @@
1
+ export const REQUEST_TIMEOUT_MS = 30_000;
2
+ export class ServiceNowError extends Error {
3
+ code;
4
+ resolution;
5
+ constructor(message, code, resolution) {
6
+ super(message);
7
+ this.code = code;
8
+ this.resolution = resolution;
9
+ this.name = 'ServiceNowError';
10
+ }
11
+ }
12
+ /**
13
+ * Regex for a valid single-label ServiceNow instance name:
14
+ * lowercase alphanumeric with optional hyphens between segments.
15
+ */
16
+ export const SINGLE_LABEL_INSTANCE_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,14 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ type ToolHandler<T> = (args: T, extra: unknown) => Promise<CallToolResult>;
3
+ /**
4
+ * Wraps a tool handler with standard error handling.
5
+ *
6
+ * - On success: returns the string result as a text content block.
7
+ * - On ServiceNowError: returns a structured JSON error with code and resolution.
8
+ * - On unknown error: returns a generic error message.
9
+ *
10
+ * Secrets are never exposed in error messages.
11
+ */
12
+ export declare function withErrorHandling<T>(fn: (args: T, extra: unknown) => Promise<string>): ToolHandler<T>;
13
+ export {};
14
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,42 @@
1
+ import { ServiceNowError } from './types.js';
2
+ /**
3
+ * Wraps a tool handler with standard error handling.
4
+ *
5
+ * - On success: returns the string result as a text content block.
6
+ * - On ServiceNowError: returns a structured JSON error with code and resolution.
7
+ * - On unknown error: returns a generic error message.
8
+ *
9
+ * Secrets are never exposed in error messages.
10
+ */
11
+ export function withErrorHandling(fn) {
12
+ return async (args, extra) => {
13
+ try {
14
+ const result = await fn(args, extra);
15
+ return { content: [{ type: 'text', text: result }] };
16
+ }
17
+ catch (error) {
18
+ if (error instanceof ServiceNowError) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify({
24
+ ok: false,
25
+ error: error.message,
26
+ code: error.code,
27
+ resolution: error.resolution,
28
+ }),
29
+ },
30
+ ],
31
+ isError: true,
32
+ };
33
+ }
34
+ const errorMessage = error instanceof Error ? error.message : String(error);
35
+ return {
36
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: errorMessage }) }],
37
+ isError: true,
38
+ };
39
+ }
40
+ };
41
+ }
42
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mindstone-engineering/mcp-server-servicenow",
3
+ "version": "0.1.0",
4
+ "description": "ServiceNow ITSM MCP server for Model Context Protocol hosts",
5
+ "license": "FSL-1.1-MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-server-servicenow": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "!dist/**/*.map"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/nspr-io/mcp-servers.git",
17
+ "directory": "connectors/servicenow"
18
+ },
19
+ "homepage": "https://github.com/nspr-io/mcp-servers/tree/main/connectors/servicenow",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc && shx chmod +x dist/index.js",
25
+ "prepare": "npm run build",
26
+ "watch": "tsc --watch",
27
+ "start": "node dist/index.js",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:coverage": "vitest run --coverage"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.26.0",
34
+ "zod": "^3.23.0"
35
+ },
36
+ "devDependencies": {
37
+ "@mindstone-engineering/mcp-test-harness": "file:../../test-harness",
38
+ "@types/node": "^22",
39
+ "@vitest/coverage-v8": "^4.1.3",
40
+ "msw": "^2.13.2",
41
+ "shx": "^0.3.4",
42
+ "typescript": "^5.8.2",
43
+ "vitest": "^4.1.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ }
48
+ }