ServiceNow MCP

Use this integration to connect securely with a ServiceNow Model Context Protocol (MCP) server and access its tools in real time.

Case Management · ServiceNow MCP MCP

Details

IDServiceNow MCP
ProviderServiceNow
CategoryCase Management
From Version8.13.0
Docker Imagedemisto/mcp:1.0.0.10120494
Supported ModulesAttack Surface Management Exposure Management Agentix Cortex Cloud Cloud Runtime Security Cloud Posture Security XSIAM EDR

Connection settings

Parameter Name Type Required Default
ServiceNow Instance URL instance_url Short text Yes
MCP Server Name server_name Short text Yes sn_mcp_server_default
Client ID oauth_credentials Credentials Yes
Authorization code auth_code Credentials No
Redirect URI redirect_uri Short text No https://oproxy.demisto.ninja/authcode
Trust any certificate (not secure) insecure Boolean No

Commands (4)

An MCP server publishes no fixed command set: list-tools and call-tool are the protocol plumbing, and the tools they reach are discovered from the remote server when the instance connects. The pack cannot name them ahead of time, so neither can this page.

  • call-tool

    Calls a specific tool on the MCP server with optional input parameters.

  • list-tools

    Retrieves a list of available tools in the ServiceNow MCP server.

  • servicenow-mcp-auth-test

    Test the authentication configuration with the MCP server.

  • servicenow-mcp-generate-login-url

    Generate an authentication login URL.

import pytest
from pytest_mock import MockerFixture

import demistomock as demisto
from ServiceNowMCP import main, build_servicenow_urls, validate_required_params, DEFAULT_MCP_SERVER_NAME


VALID_PARAMS = {
    "instance_url": "https://dev12345.service-now.com",
    "server_name": "sn_mcp_server_default",
    "oauth_credentials": {"identifier": "test_client_id", "password": "test_client_secret"},
    "auth_code": {"password": "test_auth_code"},
    "redirect_uri": "https://oproxy.demisto.ninja/authcode",
    "insecure": False,
}


class TestBuildServiceNowUrls:
    """Unit tests for build_servicenow_urls."""

    def test_build_urls_with_explicit_server_name(self):
        """Given: A full ServiceNow instance URL and explicit server name.
        When: build_servicenow_urls is called.
        Then: Correct base_url, authorization_endpoint, and token_endpoint are returned.
        """
        base_url, auth_endpoint, token_endpoint = build_servicenow_urls("https://dev12345.service-now.com", "my_server")

        assert base_url == "https://dev12345.service-now.com/sncapps/mcp-server/mcp/my_server"
        assert auth_endpoint == "https://dev12345.service-now.com/oauth_auth.do"
        assert token_endpoint == "https://dev12345.service-now.com/oauth_token.do"

    def test_build_urls_with_empty_server_name_uses_default(self):
        """Given: A full ServiceNow instance URL and an empty server name.
        When: build_servicenow_urls is called.
        Then: The default Quickstart server name is used in the base URL.
        """
        base_url, _, _ = build_servicenow_urls("https://dev12345.service-now.com", "")

        assert base_url == f"https://dev12345.service-now.com/sncapps/mcp-server/mcp/{DEFAULT_MCP_SERVER_NAME}"

    def test_build_urls_strips_whitespace_and_trailing_slash(self):
        """Given: An instance URL with surrounding whitespace and a trailing slash.
        When: build_servicenow_urls is called.
        Then: The URLs are built with the cleaned instance URL.
        """
        base_url, auth_endpoint, token_endpoint = build_servicenow_urls(
            "  https://dev12345.service-now.com/  ", "  custom_server  "
        )

        assert base_url == "https://dev12345.service-now.com/sncapps/mcp-server/mcp/custom_server"
        assert auth_endpoint == "https://dev12345.service-now.com/oauth_auth.do"
        assert token_endpoint == "https://dev12345.service-now.com/oauth_token.do"


class TestValidateRequiredParams:
    """Unit tests for validate_required_params."""

    def test_valid_params_passes(self):
        """Given: All required parameters are provided.
        When: validate_required_params is called.
        Then: No exception is raised.
        """
        validate_required_params("https://dev12345.service-now.com", "cid", "csecret")

    def test_missing_instance_raises(self):
        """Given: The instance URL is missing.
        When: validate_required_params is called.
        Then: A ValueError is raised mentioning the instance requirement.
        """
        with pytest.raises(ValueError, match="ServiceNow Instance URL"):
            validate_required_params("", "cid", "csecret")

    def test_missing_client_id_raises(self):
        """Given: The Client ID is missing.
        When: validate_required_params is called.
        Then: A ValueError is raised mentioning OAuth credentials.
        """
        with pytest.raises(ValueError, match="Client ID and Client Secret"):
            validate_required_params("https://dev12345.service-now.com", "", "csecret")

    def test_missing_client_secret_raises(self):
        """Given: The Client Secret is missing.
        When: validate_required_params is called.
        Then: A ValueError is raised mentioning OAuth credentials.
        """
        with pytest.raises(ValueError, match="Client ID and Client Secret"):
            validate_required_params("https://dev12345.service-now.com", "cid", "")


class TestMain:
    """Unit tests for the main function of ServiceNowMCP."""

    @pytest.mark.asyncio
    async def test_test_module_command(self, mocker: MockerFixture):
        """Given: The test-module command is called for an OAuth integration.
        When: Main function processes the command.
        Then: return_error is called with a message directing the user to the auth-test command.
        """

        async def mock_close():
            pass

        mock_client = mocker.MagicMock()
        mock_client.close = mock_close
        mocker.patch("ServiceNowMCP.Client", return_value=mock_client)
        mocker.patch.object(demisto, "params", return_value=VALID_PARAMS)
        mocker.patch.object(demisto, "args", return_value={})
        mocker.patch.object(demisto, "command", return_value="test-module")
        mock_return_error = mocker.patch("ServiceNowMCP.return_error")

        await main()

        mock_return_error.assert_called_once()
        error_call = mock_return_error.call_args[0][0]
        assert "Test module is unavailable for this integration" in error_call
        assert "servicenow-mcp-auth-test" in error_call

    @pytest.mark.asyncio
    async def test_list_tools_command(self, mocker: MockerFixture):
        """Given: The list-tools command is called with valid params.
        When: Main function processes the command.
        Then: The client's list_tools method is called and results are returned.
        """

        async def mock_list_tools(server_name):
            return {"tools": []}

        async def mock_close():
            pass

        mock_client = mocker.MagicMock()
        mock_client.list_tools = mock_list_tools
        mock_client.close = mock_close
        mocker.patch("ServiceNowMCP.Client", return_value=mock_client)
        mocker.patch.object(demisto, "params", return_value=VALID_PARAMS)
        mocker.patch.object(demisto, "args", return_value={})
        mocker.patch.object(demisto, "command", return_value="list-tools")
        mock_return_results = mocker.patch("ServiceNowMCP.return_results")

        await main()

        mock_return_results.assert_called_once_with({"tools": []})

    @pytest.mark.asyncio
    async def test_call_tool_command_with_arguments(self, mocker: MockerFixture):
        """Given: The call-tool command is called with a tool name and arguments.
        When: Main function processes the command.
        Then: The client's call_tool method is called with the provided parameters.
        """

        captured: dict = {}

        async def mock_call_tool(name, arguments):
            captured["name"] = name
            captured["arguments"] = arguments
            return {"result": "success"}

        async def mock_close():
            pass

        mock_client = mocker.MagicMock()
        mock_client.call_tool = mock_call_tool
        mock_client.close = mock_close
        mocker.patch("ServiceNowMCP.Client", return_value=mock_client)
        mocker.patch.object(demisto, "params", return_value=VALID_PARAMS)
        mocker.patch.object(demisto, "args", return_value={"name": "lookup_incident", "arguments": '{"number": "INC0010001"}'})
        mocker.patch.object(demisto, "command", return_value="call-tool")
        mock_return_results = mocker.patch("ServiceNowMCP.return_results")

        await main()

        assert captured == {"name": "lookup_incident", "arguments": '{"number": "INC0010001"}'}
        mock_return_results.assert_called_once_with({"result": "success"})

    @pytest.mark.asyncio
    async def test_call_tool_command_without_arguments(self, mocker: MockerFixture):
        """Given: The call-tool command is called with only a tool name (no arguments).
        When: Main function processes the command.
        Then: The client's call_tool method is called with an empty arguments string.
        """

        captured: dict = {}

        async def mock_call_tool(name, arguments):
            captured["name"] = name
            captured["arguments"] = arguments
            return {"result": "ok"}

        async def mock_close():
            pass

        mock_client = mocker.MagicMock()
        mock_client.call_tool = mock_call_tool
        mock_client.close = mock_close
        mocker.patch("ServiceNowMCP.Client", return_value=mock_client)
        mocker.patch.object(demisto, "params", return_value=VALID_PARAMS)
        mocker.patch.object(demisto, "args", return_value={"name": "list_incidents"})
        mocker.patch.object(demisto, "command", return_value="call-tool")
        mock_return_results = mocker.patch("ServiceNowMCP.return_results")

        await main()

        assert captured == {"name": "list_incidents", "arguments": ""}
        mock_return_results.assert_called_once_with({"result": "ok"})

    @pytest.mark.asyncio
    async def test_auth_test_command(self, mocker: MockerFixture):
        """Given: The servicenow-mcp-auth-test command is called.
        When: Main function processes the command.
        Then: The client's test_connection method is called with auth_test=True.
        """

        captured: dict = {}

        async def mock_test_connection(auth_test=False):
            captured["auth_test"] = auth_test
            return {"status": "authenticated"}

        async def mock_close():
            pass

        mock_client = mocker.MagicMock()
        mock_client.test_connection = mock_test_connection
        mock_client.close = mock_close
        mocker.patch("ServiceNowMCP.Client", return_value=mock_client)
        mocker.patch.object(demisto, "params", return_value=VALID_PARAMS)
        mocker.patch.object(demisto, "args", return_value={})
        mocker.patch.object(demisto, "command", return_value="servicenow-mcp-auth-test")
        mock_return_results = mocker.patch("ServiceNowMCP.return_results")

        await main()

        assert captured == {"auth_test": True}
        mock_return_results.assert_called_once_with({"status": "authenticated"})

    @pytest.mark.asyncio
    async def test_generate_login_url_command(self, mocker: MockerFixture):
        """Given: The servicenow-mcp-generate-login-url command is called.
        When: Main function processes the command.
        Then: generate_login_url is called with the ServiceNow authorization endpoint and troubleshooting enabled.
        """

        captured_kwargs: dict = {}

        async def mock_generate_login_url(*args, **kwargs):
            captured_kwargs.update(kwargs)
            return {"login_url": "https://dev12345.service-now.com/oauth_auth.do?..."}

        async def mock_close():
            pass

        mock_client = mocker.MagicMock()
        mock_client.close = mock_close
        mocker.patch("ServiceNowMCP.Client", return_value=mock_client)
        mocker.patch("ServiceNowMCP.generate_login_url", side_effect=mock_generate_login_url)
        mocker.patch.object(demisto, "params", return_value=VALID_PARAMS)
        mocker.patch.object(demisto, "args", return_value={})
        mocker.patch.object(demisto, "command", return_value="servicenow-mcp-generate-login-url")
        mock_return_results = mocker.patch("ServiceNowMCP.return_results")

        await main()

        assert captured_kwargs["authorization_endpoint"] == "https://dev12345.service-now.com/oauth_auth.do"
        assert captured_kwargs["redirect_uri"] == VALID_PARAMS["redirect_uri"]
        assert captured_kwargs["troubleshooting_redirect"] is True
        mock_return_results.assert_called_once_with({"login_url": "https://dev12345.service-now.com/oauth_auth.do?..."})

    @pytest.mark.asyncio
    async def test_unknown_command(self, mocker: MockerFixture):
        """Given: An unknown command is called.
        When: Main function processes the command.
        Then: A NotImplementedError is raised and return_error is called.
        """

        async def mock_close():
            pass

        mock_client = mocker.MagicMock()
        mock_client.close = mock_close
        mocker.patch("ServiceNowMCP.Client", return_value=mock_client)
        mocker.patch.object(demisto, "params", return_value=VALID_PARAMS)
        mocker.patch.object(demisto, "args", return_value={})
        mocker.patch.object(demisto, "command", return_value="unknown-command")
        mock_return_error = mocker.patch("ServiceNowMCP.return_error")

        await main()

        mock_return_error.assert_called_once()
        error_call = mock_return_error.call_args[0][0]
        assert "Command unknown-command is not implemented" in error_call

    @pytest.mark.asyncio
    async def test_exception_handling(self, mocker: MockerFixture):
        """Given: An exception occurs during command processing.
        When: Main function processes the command.
        Then: The exception is caught and return_error is called with the error message.
        """

        async def mock_list_tools_with_error(server_name):
            raise Exception("Connection failed")

        async def mock_close():
            pass

        mock_client = mocker.MagicMock()
        mock_client.list_tools = mock_list_tools_with_error
        mock_client.close = mock_close
        mocker.patch("ServiceNowMCP.Client", return_value=mock_client)
        mocker.patch.object(demisto, "params", return_value=VALID_PARAMS)
        mocker.patch.object(demisto, "args", return_value={})
        mocker.patch.object(demisto, "command", return_value="list-tools")
        mock_return_error = mocker.patch("ServiceNowMCP.return_error")

        await main()

        mock_return_error.assert_called_once()
        error_call = mock_return_error.call_args[0][0]
        assert "Failed to execute list-tools command" in error_call
        assert "Connection failed" in error_call

    @pytest.mark.asyncio
    async def test_validation_failure_missing_instance(self, mocker: MockerFixture):
        """Given: The integration is configured without a ServiceNow instance.
        When: Main function processes any command.
        Then: validate_required_params raises ValueError and return_error is called.
        """
        params = dict(VALID_PARAMS)
        params["instance_url"] = ""

        mocker.patch.object(demisto, "params", return_value=params)
        mocker.patch.object(demisto, "args", return_value={})
        mocker.patch.object(demisto, "command", return_value="list-tools")
        mock_return_error = mocker.patch("ServiceNowMCP.return_error")

        await main()

        mock_return_error.assert_called_once()
        error_call = mock_return_error.call_args[0][0]
        assert "ServiceNow Instance" in error_call

    @pytest.mark.asyncio
    async def test_validation_failure_missing_oauth_credentials(self, mocker: MockerFixture):
        """Given: The integration is configured without OAuth credentials.
        When: Main function processes any command.
        Then: validate_required_params raises ValueError and return_error is called.
        """
        params = dict(VALID_PARAMS)
        params["oauth_credentials"] = {"identifier": "", "password": ""}

        mocker.patch.object(demisto, "params", return_value=params)
        mocker.patch.object(demisto, "args", return_value={})
        mocker.patch.object(demisto, "command", return_value="list-tools")
        mock_return_error = mocker.patch("ServiceNowMCP.return_error")

        await main()

        mock_return_error.assert_called_once()
        error_call = mock_return_error.call_args[0][0]
        assert "Client ID and Client Secret" in error_call