#
tokens: 49564/50000 38/79 files (page 1/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 5. Use http://codebase.md/osomai/servicenow-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursorrules
├── .DS_Store
├── .env.example
├── .gitignore
├── config
│   └── tool_packages.yaml
├── debug_workflow_api.py
├── Dockerfile
├── docs
│   ├── catalog_optimization_plan.md
│   ├── catalog_variables.md
│   ├── catalog.md
│   ├── change_management.md
│   ├── changeset_management.md
│   ├── incident_management.md
│   ├── knowledge_base.md
│   ├── user_management.md
│   └── workflow_management.md
├── examples
│   ├── catalog_integration_test.py
│   ├── catalog_optimization_example.py
│   ├── change_management_demo.py
│   ├── changeset_management_demo.py
│   ├── claude_catalog_demo.py
│   ├── claude_desktop_config.json
│   ├── claude_incident_demo.py
│   ├── debug_workflow_api.py
│   ├── wake_servicenow_instance.py
│   └── workflow_management_demo.py
├── LICENSE
├── prompts
│   └── add_servicenow_mcp_tool.md
├── pyproject.toml
├── README.md
├── scripts
│   ├── check_pdi_info.py
│   ├── check_pdi_status.py
│   ├── install_claude_desktop.sh
│   ├── setup_api_key.py
│   ├── setup_auth.py
│   ├── setup_oauth.py
│   ├── setup.sh
│   └── test_connection.py
├── src
│   ├── .DS_Store
│   └── servicenow_mcp
│       ├── __init__.py
│       ├── .DS_Store
│       ├── auth
│       │   ├── __init__.py
│       │   └── auth_manager.py
│       ├── cli.py
│       ├── server_sse.py
│       ├── server.py
│       ├── tools
│       │   ├── __init__.py
│       │   ├── catalog_optimization.py
│       │   ├── catalog_tools.py
│       │   ├── catalog_variables.py
│       │   ├── change_tools.py
│       │   ├── changeset_tools.py
│       │   ├── epic_tools.py
│       │   ├── incident_tools.py
│       │   ├── knowledge_base.py
│       │   ├── project_tools.py
│       │   ├── script_include_tools.py
│       │   ├── scrum_task_tools.py
│       │   ├── story_tools.py
│       │   ├── user_tools.py
│       │   └── workflow_tools.py
│       └── utils
│           ├── __init__.py
│           ├── config.py
│           └── tool_utils.py
├── tests
│   ├── test_catalog_optimization.py
│   ├── test_catalog_resources.py
│   ├── test_catalog_tools.py
│   ├── test_catalog_variables.py
│   ├── test_change_tools.py
│   ├── test_changeset_resources.py
│   ├── test_changeset_tools.py
│   ├── test_config.py
│   ├── test_incident_tools.py
│   ├── test_knowledge_base.py
│   ├── test_script_include_resources.py
│   ├── test_script_include_tools.py
│   ├── test_server_catalog_optimization.py
│   ├── test_server_catalog.py
│   ├── test_server_workflow.py
│   ├── test_user_tools.py
│   ├── test_workflow_tools_direct.py
│   ├── test_workflow_tools_params.py
│   └── test_workflow_tools.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
1 | SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com
2 | SERVICENOW_USERNAME=your-username
3 | SERVICENOW_PASSWORD=your-password 
```

--------------------------------------------------------------------------------
/.cursorrules:
--------------------------------------------------------------------------------

```
 1 | # Python project structure rules
 2 | # This file defines the structure for a Python project with similar organization to the ServiceNow MCP project
 3 | 
 4 | # Root directory structure
 5 | root/
 6 |   ├── src/                   # Source code directory
 7 |   │   └── servicenow_mcp/    # Main package directory
 8 |   │       ├── __init__.py    # Package initialization
 9 |   │       ├── server.py      # Server implementation
10 |   │       ├── cli.py         # CLI implementation
11 |   │       ├── auth/          # Authentication related code
12 |   │       ├── tools/         # Tool implementations
13 |   │       ├── utils/         # Utility functions
14 |   │       └── resources/     # Resource definitions
15 |   ├── tests/                 # Test directory
16 |   │   ├── __init__.py
17 |   │   └── test_*.py         # Test files
18 |   ├── docs/                  # Documentation
19 |   ├── examples/              # Example code
20 |   ├── scripts/               # Utility scripts
21 |   ├── .env                   # Environment variables
22 |   ├── .gitignore            # Git ignore rules
23 |   ├── LICENSE               # License file
24 |   ├── README.md             # Project documentation
25 |   ├── pyproject.toml        # Project configuration
26 |   └── uv.lock              # Dependency lock file
27 | 
28 | # File naming conventions
29 | *.py:
30 |   - Use snake_case for file names
31 |   - Test files should start with test_
32 |   - Main package files should be descriptive of their purpose
33 | 
34 | # Directory naming conventions
35 | directories:
36 |   - Use snake_case for directory names
37 |   - Keep directory names lowercase
38 |   - Use plural form for directories containing multiple items
39 | 
40 | # Import structure
41 | imports:
42 |   - Group imports in the following order:
43 |     1. Standard library imports
44 |     2. Third-party imports
45 |     3. Local application imports
46 |   - Use absolute imports for external packages
47 |   - Use relative imports for local modules
48 | 
49 | # Testing conventions
50 | tests:
51 |   - Each test file should correspond to a module in the source code
52 |   - Test classes should be prefixed with Test
53 |   - Test methods should be prefixed with test_
54 |   - Use pytest fixtures for common setup
55 | 
56 | # Documentation
57 | docs:
58 |   - Include docstrings for all public functions and classes
59 |   - Use Google style docstrings
60 |   - Keep README.md up to date with project information
61 |   - Document API endpoints and usage in docs/
62 | 
63 | # Code style
64 | style:
65 |   - Follow PEP 8 guidelines
66 |   - Use type hints for function parameters and return values
67 |   - Keep functions focused and single-purpose
68 |   - Use meaningful variable and function names
69 |   - Add comments for complex logic
70 | 
71 | # Version control
72 | git:
73 |   - Use meaningful commit messages
74 |   - Keep commits focused and atomic
75 |   - Include relevant issue numbers in commit messages
76 |   - Use feature branches for new development
77 | 
78 | # Dependencies
79 | dependencies:
80 |   - Use pyproject.toml for project configuration
81 |   - Keep dependencies up to date
82 |   - Use uv.lock for deterministic builds
83 |   - Document all external dependencies in README.md 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | # Distribution / packaging
 10 | .Python
 11 | build/
 12 | develop-eggs/
 13 | dist/
 14 | downloads/
 15 | eggs/
 16 | .eggs/
 17 | lib/
 18 | lib64/
 19 | parts/
 20 | sdist/
 21 | var/
 22 | wheels/
 23 | share/python-wheels/
 24 | *.egg-info/
 25 | .installed.cfg
 26 | *.egg
 27 | MANIFEST
 28 | 
 29 | # PyInstaller
 30 | #  Usually these files are written by a python script from a template
 31 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 32 | *.manifest
 33 | *.spec
 34 | 
 35 | # Installer logs
 36 | pip-log.txt
 37 | pip-delete-this-directory.txt
 38 | 
 39 | # Unit test / coverage reports
 40 | htmlcov/
 41 | .tox/
 42 | .nox/
 43 | .coverage
 44 | .coverage.*
 45 | .cache
 46 | nosetests.xml
 47 | coverage.xml
 48 | *.cover
 49 | *.py,cover
 50 | .hypothesis/
 51 | .pytest_cache/
 52 | cover/
 53 | 
 54 | # Translations
 55 | *.mo
 56 | *.pot
 57 | 
 58 | # Django stuff:
 59 | *.log
 60 | local_settings.py
 61 | db.sqlite3
 62 | db.sqlite3-journal
 63 | 
 64 | # Flask stuff:
 65 | instance/
 66 | .webassets-cache
 67 | 
 68 | # Scrapy stuff:
 69 | .scrapy
 70 | 
 71 | # Sphinx documentation
 72 | docs/_build/
 73 | 
 74 | # PyBuilder
 75 | .pybuilder/
 76 | target/
 77 | 
 78 | # Jupyter Notebook
 79 | .ipynb_checkpoints
 80 | 
 81 | # IPython
 82 | profile_default/
 83 | ipython_config.py
 84 | 
 85 | # pyenv
 86 | #   For a library or package, you might want to ignore these files since the code is
 87 | #   intended to run in multiple environments; otherwise, check them in:
 88 | .python-version
 89 | 
 90 | # pipenv
 91 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 92 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
 93 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
 94 | #   install all needed dependencies.
 95 | #Pipfile.lock
 96 | 
 97 | # UV
 98 | #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 99 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
100 | #   commonly ignored for libraries.
101 | #uv.lock
102 | 
103 | # poetry
104 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
106 | #   commonly ignored for libraries.
107 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 | 
110 | # pdm
111 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | #   in version control.
115 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 | 
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 | 
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 | 
127 | # SageMath parsed files
128 | *.sage.py
129 | 
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 | 
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 | 
143 | # Rope project settings
144 | .ropeproject
145 | 
146 | # mkdocs documentation
147 | /site
148 | 
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 | 
154 | # Pyre type checker
155 | .pyre/
156 | 
157 | # pytype static type analyzer
158 | .pytype/
159 | 
160 | # Cython debug symbols
161 | cython_debug/
162 | 
163 | # PyCharm
164 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
167 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 | 
170 | # PyPI configuration file
171 | .pypirc
172 | 
173 | # IDE files
174 | .idea/
175 | .vscode/
176 | *.swp
177 | *.swo
178 | 
179 | # Project specific
180 | .env.local
181 | .env.development
182 | .env.test
183 | .env.production
184 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/osomai-servicenow-mcp-badge.png)](https://mseep.ai/app/osomai-servicenow-mcp)
  2 | 
  3 | # ServiceNow MCP Server
  4 | 
  5 | A Model Completion Protocol (MCP) server implementation for ServiceNow, allowing Claude to interact with ServiceNow instances.
  6 | 
  7 | <a href="https://glama.ai/mcp/servers/@osomai/servicenow-mcp">
  8 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/@osomai/servicenow-mcp/badge" alt="ServiceNow Server MCP server" />
  9 | </a>
 10 | 
 11 | ## Overview
 12 | 
 13 | This project implements an MCP server that enables Claude to connect to ServiceNow instances, retrieve data, and perform actions through the ServiceNow API. It serves as a bridge between Claude and ServiceNow, allowing for seamless integration.
 14 | 
 15 | ## Features
 16 | 
 17 | - Connect to ServiceNow instances using various authentication methods (Basic, OAuth, API Key)
 18 | - Query ServiceNow records and tables
 19 | - Create, update, and delete ServiceNow records
 20 | - Execute ServiceNow scripts and workflows
 21 | - Access and query the ServiceNow Service Catalog
 22 | - Analyze and optimize the ServiceNow Service Catalog
 23 | - Debug mode for troubleshooting
 24 | - Support for both stdio and Server-Sent Events (SSE) communication
 25 | 
 26 | ## Installation
 27 | 
 28 | ### Prerequisites
 29 | 
 30 | - Python 3.11 or higher
 31 | - A ServiceNow instance with appropriate access credentials
 32 | 
 33 | ### Setup
 34 | 
 35 | 1. Clone this repository:
 36 |    ```
 37 |    git clone https://github.com/echelon-ai-labs/servicenow-mcp.git
 38 |    cd servicenow-mcp
 39 |    ```
 40 | 
 41 | 2. Create a virtual environment and install the package:
 42 |    ```
 43 |    python -m venv .venv
 44 |    source .venv/bin/activate  # On Windows: .venv\Scripts\activate
 45 |    pip install -e .
 46 |    ```
 47 | 
 48 | 3. Create a `.env` file with your ServiceNow credentials:
 49 |    ```
 50 |    SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com
 51 |    SERVICENOW_USERNAME=your-username
 52 |    SERVICENOW_PASSWORD=your-password
 53 |    SERVICENOW_AUTH_TYPE=basic  # or oauth, api_key
 54 |    ```
 55 | 
 56 | ## Usage
 57 | 
 58 | ### Standard (stdio) Mode
 59 | 
 60 | To start the MCP server:
 61 | 
 62 | ```
 63 | python -m servicenow_mcp.cli
 64 | ```
 65 | 
 66 | Or with environment variables:
 67 | 
 68 | ```
 69 | SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com SERVICENOW_USERNAME=your-username SERVICENOW_PASSWORD=your-password SERVICENOW_AUTH_TYPE=basic python -m servicenow_mcp.cli
 70 | ```
 71 | 
 72 | ### Server-Sent Events (SSE) Mode
 73 | 
 74 | The ServiceNow MCP server can also run as a web server using Server-Sent Events (SSE) for communication, which allows for more flexible integration options.
 75 | 
 76 | #### Starting the SSE Server
 77 | 
 78 | You can start the SSE server using the provided CLI:
 79 | 
 80 | ```
 81 | servicenow-mcp-sse --instance-url=https://your-instance.service-now.com --username=your-username --password=your-password
 82 | ```
 83 | 
 84 | By default, the server will listen on `0.0.0.0:8080`. You can customize the host and port:
 85 | 
 86 | ```
 87 | servicenow-mcp-sse --host=127.0.0.1 --port=8000
 88 | ```
 89 | 
 90 | #### Connecting to the SSE Server
 91 | 
 92 | The SSE server exposes two main endpoints:
 93 | 
 94 | - `/sse` - The SSE connection endpoint
 95 | - `/messages/` - The endpoint for sending messages to the server
 96 | 
 97 | #### Example
 98 | 
 99 | See the `examples/sse_server_example.py` file for a complete example of setting up and running the SSE server.
100 | 
101 | ```python
102 | from servicenow_mcp.server import ServiceNowMCP
103 | from servicenow_mcp.server_sse import create_starlette_app
104 | from servicenow_mcp.utils.config import ServerConfig, AuthConfig, AuthType, BasicAuthConfig
105 | import uvicorn
106 | 
107 | # Create server configuration
108 | config = ServerConfig(
109 |     instance_url="https://your-instance.service-now.com",
110 |     auth=AuthConfig(
111 |         type=AuthType.BASIC,
112 |         config=BasicAuthConfig(
113 |             username="your-username",
114 |             password="your-password"
115 |         )
116 |     ),
117 |     debug=True,
118 | )
119 | 
120 | # Create ServiceNow MCP server
121 | servicenow_mcp = ServiceNowMCP(config)
122 | 
123 | # Create Starlette app with SSE transport
124 | app = create_starlette_app(servicenow_mcp, debug=True)
125 | 
126 | # Start the web server
127 | uvicorn.run(app, host="0.0.0.0", port=8080)
128 | ```
129 | 
130 | ## Tool Packaging (Optional)
131 | 
132 | To manage the number of tools exposed to the language model (especially in environments with limits), the ServiceNow MCP server supports loading subsets of tools called "packages". This is controlled via the `MCP_TOOL_PACKAGE` environment variable.
133 | 
134 | ### Configuration
135 | 
136 | 1.  **Environment Variable:** Set the `MCP_TOOL_PACKAGE` environment variable to the name of the desired package.
137 |     ```bash
138 |     export MCP_TOOL_PACKAGE=catalog_builder
139 |     ```
140 | 2.  **Package Definitions:** The available packages and the tools they include are defined in `config/tool_packages.yaml`. You can customize this file to create your own packages.
141 | 
142 | ### Behavior
143 | 
144 | -   If `MCP_TOOL_PACKAGE` is set to a valid package name defined in `config/tool_packages.yaml`, only the tools listed in that package will be loaded.
145 | -   If `MCP_TOOL_PACKAGE` is **not set** or is empty, the `full` package (containing all tools) is loaded by default.
146 | -   If `MCP_TOOL_PACKAGE` is set to an invalid package name, the `none` package is loaded (no tools except `list_tool_packages`), and a warning is logged.
147 | -   Setting `MCP_TOOL_PACKAGE=none` explicitly loads no tools (except `list_tool_packages`).
148 | 
149 | ### Available Packages (Default)
150 | 
151 | The default `config/tool_packages.yaml` includes the following role-based packages:
152 | 
153 | -   `service_desk`: Tools for incident handling and basic user/knowledge lookup.
154 | -   `catalog_builder`: Tools for creating and managing service catalog items, categories, variables, and related scripting (UI Policies, User Criteria).
155 | -   `change_coordinator`: Tools for managing the change request lifecycle, including tasks and approvals.
156 | -   `knowledge_author`: Tools for creating and managing knowledge bases, categories, and articles.
157 | -   `platform_developer`: Tools for server-side scripting (Script Includes), workflow development, and deployment (Changesets).
158 | -   `system_administrator`: Tools for user/group management and viewing system logs.
159 | -   `agile_management`: Tools for managing user stories, epics, scrum tasks, and projects.
160 | -   `full`: Includes all available tools (default).
161 | -   `none`: Includes no tools (except `list_tool_packages`).
162 | 
163 | ### Introspection Tool
164 | 
165 | -   **`list_tool_packages`**: Lists all available tool package names defined in the configuration and shows the currently loaded package. This tool is available in all packages except `none`.
166 | 
167 | ## Available Tools
168 | 
169 | **Note:** The availability of the following tools depends on the loaded tool package (see Tool Packaging section above). By default (`full` package), all tools are available.
170 | 
171 | #### Incident Management Tools
172 | 
173 | 1. **create_incident** - Create a new incident in ServiceNow
174 | 2. **update_incident** - Update an existing incident in ServiceNow
175 | 3. **add_comment** - Add a comment to an incident in ServiceNow
176 | 4. **resolve_incident** - Resolve an incident in ServiceNow
177 | 5. **list_incidents** - List incidents from ServiceNow
178 | 
179 | #### Service Catalog Tools
180 | 
181 | 1. **list_catalog_items** - List service catalog items from ServiceNow
182 | 2. **get_catalog_item** - Get a specific service catalog item from ServiceNow
183 | 3. **list_catalog_categories** - List service catalog categories from ServiceNow
184 | 4. **create_catalog_category** - Create a new service catalog category in ServiceNow
185 | 5. **update_catalog_category** - Update an existing service catalog category in ServiceNow
186 | 6. **move_catalog_items** - Move catalog items between categories in ServiceNow
187 | 7. **create_catalog_item_variable** - Create a new variable (form field) for a catalog item
188 | 8. **list_catalog_item_variables** - List all variables for a catalog item
189 | 9. **update_catalog_item_variable** - Update an existing variable for a catalog item
190 | 10. **list_catalogs** - List service catalogs from ServiceNow
191 | 
192 | #### Catalog Optimization Tools
193 | 
194 | 1. **get_optimization_recommendations** - Get recommendations for optimizing the service catalog
195 | 2. **update_catalog_item** - Update a service catalog item
196 | 
197 | #### Change Management Tools
198 | 
199 | 1. **create_change_request** - Create a new change request in ServiceNow
200 | 2. **update_change_request** - Update an existing change request
201 | 3. **list_change_requests** - List change requests with filtering options
202 | 4. **get_change_request_details** - Get detailed information about a specific change request
203 | 5. **add_change_task** - Add a task to a change request
204 | 6. **submit_change_for_approval** - Submit a change request for approval
205 | 7. **approve_change** - Approve a change request
206 | 8. **reject_change** - Reject a change request
207 | 
208 | #### Agile Management Tools
209 | 
210 | ##### Story Management
211 | 1. **create_story** - Create a new user story in ServiceNow
212 | 2. **update_story** - Update an existing user story in ServiceNow
213 | 3. **list_stories** - List user stories with filtering options
214 | 4. **create_story_dependency** - Create a dependency between two stories
215 | 5. **delete_story_dependency** - Delete a dependency between stories
216 | 
217 | ##### Epic Management
218 | 1. **create_epic** - Create a new epic in ServiceNow
219 | 2. **update_epic** - Update an existing epic in ServiceNow
220 | 3. **list_epics** - List epics from ServiceNow with filtering options
221 | 
222 | ##### Scrum Task Management
223 | 1. **create_scrum_task** - Create a new scrum task in ServiceNow
224 | 2. **update_scrum_task** - Update an existing scrum task in ServiceNow
225 | 3. **list_scrum_tasks** - List scrum tasks from ServiceNow with filtering options
226 | 
227 | ##### Project Management
228 | 1. **create_project** - Create a new project in ServiceNow
229 | 2. **update_project** - Update an existing project in ServiceNow
230 | 3. **list_projects** - List projects from ServiceNow with filtering options
231 | 
232 | #### Workflow Management Tools
233 | 
234 | 1. **list_workflows** - List workflows from ServiceNow
235 | 2. **get_workflow** - Get a specific workflow from ServiceNow
236 | 3. **create_workflow** - Create a new workflow in ServiceNow
237 | 4. **update_workflow** - Update an existing workflow in ServiceNow
238 | 5. **delete_workflow** - Delete a workflow from ServiceNow
239 | 
240 | #### Script Include Management Tools
241 | 
242 | 1. **list_script_includes** - List script includes from ServiceNow
243 | 2. **get_script_include** - Get a specific script include from ServiceNow
244 | 3. **create_script_include** - Create a new script include in ServiceNow
245 | 4. **update_script_include** - Update an existing script include in ServiceNow
246 | 5. **delete_script_include** - Delete a script include from ServiceNow
247 | 
248 | #### Changeset Management Tools
249 | 
250 | 1. **list_changesets** - List changesets from ServiceNow with filtering options
251 | 2. **get_changeset_details** - Get detailed information about a specific changeset
252 | 3. **create_changeset** - Create a new changeset in ServiceNow
253 | 4. **update_changeset** - Update an existing changeset
254 | 5. **commit_changeset** - Commit a changeset
255 | 6. **publish_changeset** - Publish a changeset
256 | 7. **add_file_to_changeset** - Add a file to a changeset
257 | 
258 | #### Knowledge Base Management Tools
259 | 
260 | 1. **create_knowledge_base** - Create a new knowledge base in ServiceNow
261 | 2. **list_knowledge_bases** - List knowledge bases with filtering options
262 | 3. **create_category** - Create a new category in a knowledge base
263 | 4. **create_article** - Create a new knowledge article in ServiceNow
264 | 5. **update_article** - Update an existing knowledge article in ServiceNow
265 | 6. **publish_article** - Publish a knowledge article in ServiceNow
266 | 7. **list_articles** - List knowledge articles with filtering options
267 | 8. **get_article** - Get a specific knowledge article by ID
268 | 
269 | #### User Management Tools
270 | 
271 | 1. **create_user** - Create a new user in ServiceNow
272 | 2. **update_user** - Update an existing user in ServiceNow
273 | 3. **get_user** - Get a specific user by ID, username, or email
274 | 4. **list_users** - List users with filtering options
275 | 5. **create_group** - Create a new group in ServiceNow
276 | 6. **update_group** - Update an existing group in ServiceNow
277 | 7. **add_group_members** - Add members to a group in ServiceNow
278 | 8. **remove_group_members** - Remove members from a group in ServiceNow
279 | 9. **list_groups** - List groups with filtering options
280 | 
281 | #### UI Policy Tools
282 | 
283 | 1. **create_ui_policy** - Creates a ServiceNow UI Policy, typically for a Catalog Item.
284 | 2. **create_ui_policy_action** - Creates an action associated with a UI Policy to control variable states (visibility, mandatory, etc.).
285 | 
286 | ### Using the MCP CLI
287 | 
288 | The ServiceNow MCP server can be installed with the MCP CLI, which provides a convenient way to register the server with Claude.
289 | 
290 | ```bash
291 | # Install the ServiceNow MCP server with environment variables from .env file
292 | mcp install src/servicenow_mcp/server.py -f .env
293 | ```
294 | 
295 | This command will register the ServiceNow MCP server with Claude and configure it to use the environment variables from the .env file.
296 | 
297 | ### Integration with Claude Desktop
298 | 
299 | To configure the ServiceNow MCP server in Claude Desktop:
300 | 
301 | 1. Edit the Claude Desktop configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the appropriate path for your OS:
302 | 
303 | ```json
304 | {
305 |   "mcpServers": {
306 |     "ServiceNow": {
307 |       "command": "/Users/yourusername/dev/servicenow-mcp/.venv/bin/python",
308 |       "args": [
309 |         "-m",
310 |         "servicenow_mcp.cli"
311 |       ],
312 |       "env": {
313 |         "SERVICENOW_INSTANCE_URL": "https://your-instance.service-now.com",
314 |         "SERVICENOW_USERNAME": "your-username",
315 |         "SERVICENOW_PASSWORD": "your-password",
316 |         "SERVICENOW_AUTH_TYPE": "basic"
317 |       }
318 |     }
319 |   }
320 | }
321 | ```
322 | 
323 | 2. Restart Claude Desktop to apply the changes
324 | 
325 | ### Example Usage with Claude
326 | 
327 | Below are some example natural language queries you can use with Claude to interact with ServiceNow via the MCP server:
328 | 
329 | #### Incident Management Examples
330 | - "Create a new incident for a network outage in the east region"
331 | - "Update the priority of incident INC0010001 to high"
332 | - "Add a comment to incident INC0010001 saying the issue is being investigated"
333 | - "Resolve incident INC0010001 with a note that the server was restarted"
334 | - "List all high priority incidents assigned to the Network team"
335 | - "List all active P1 incidents assigned to the Network team."
336 | 
337 | #### Service Catalog Examples
338 | - "Show me all items in the service catalog"
339 | - "List all service catalog categories"
340 | - "Get details about the laptop request catalog item"
341 | - "Show me all catalog items in the Hardware category"
342 | - "Search for 'software' in the service catalog"
343 | - "Create a new category called 'Cloud Services' in the service catalog"
344 | - "Update the 'Hardware' category to rename it to 'IT Equipment'"
345 | - "Move the 'Virtual Machine' catalog item to the 'Cloud Services' category"
346 | - "Create a subcategory called 'Monitors' under the 'IT Equipment' category"
347 | - "Reorganize our catalog by moving all software items to the 'Software' category"
348 | - "Create a description field for the laptop request catalog item"
349 | - "Add a dropdown field for selecting laptop models to catalog item"
350 | - "List all form fields for the VPN access request catalog item"
351 | - "Make the department field mandatory in the software request form"
352 | - "Update the help text for the cost center field"
353 | - "Show me all service catalogs in the system"
354 | - "List all hardware catalog items."
355 | - "Find the catalog item for 'New Laptop Request'."
356 | - "Show me the variables for the 'New Laptop Request' item."
357 | - "Create a new variable named 'department_code' for the 'New Hire Setup' catalog item. Make it a mandatory string field."
358 | 
359 | #### Catalog Optimization Examples
360 | - "Analyze our service catalog and identify opportunities for improvement"
361 | - "Find catalog items with poor descriptions that need improvement"
362 | - "Identify catalog items with low usage that we might want to retire"
363 | - "Find catalog items with high abandonment rates"
364 | - "Optimize our Hardware category to improve user experience"
365 | 
366 | #### Change Management Examples
367 | - "Create a change request for server maintenance to apply security patches tomorrow night"
368 | - "Schedule a database upgrade for next Tuesday from 2 AM to 4 AM"
369 | - "Add a task to the server maintenance change for pre-implementation checks"
370 | - "Submit the server maintenance change for approval"
371 | - "Approve the database upgrade change with comment: implementation plan looks thorough"
372 | - "Show me all emergency changes scheduled for this week"
373 | - "List all changes assigned to the Network team"
374 | - "Create a normal change request to upgrade the production database server."
375 | - "Update change CHG0012345, set the state to 'Implement'."
376 | 
377 | #### Agile Management Examples
378 | - "Create a new user story for implementing a new reporting dashboard"
379 | - "Update the 'Implement a new reporting dashboard' story to set it as blocked"
380 | - "List all user stories assigned to the Data Analytics team"
381 | - "Create a dependency between the 'Implement a new reporting dashboard' story and the 'Develop data extraction pipeline' story"
382 | - "Delete the dependency between the 'Implement a new reporting dashboard' story and the 'Develop data extraction pipeline' story"
383 | - "Create a new epic called 'Data Analytics Initiatives'"
384 | - "Update the 'Data Analytics Initiatives' epic to set it as completed"
385 | - "List all epics in the 'Data Analytics' project"
386 | - "Create a new scrum task for the 'Implement a new reporting dashboard' story"
387 | - "Update the 'Develop data extraction pipeline' scrum task to set it as completed"
388 | - "List all scrum tasks in the 'Implement a new reporting dashboard' story"
389 | - "Create a new project called 'Data Analytics Initiatives'"
390 | - "Update the 'Data Analytics Initiatives' project to set it as completed"
391 | - "List all projects in the 'Data Analytics' epic"
392 | 
393 | #### Workflow Management Examples
394 | - "Show me all active workflows in ServiceNow"
395 | - "Get details about the incident approval workflow"
396 | - "List all versions of the change request workflow"
397 | - "Show me all activities in the service catalog request workflow"
398 | - "Create a new workflow for handling software license requests"
399 | - "Update the description of the incident escalation workflow"
400 | - "Activate the new employee onboarding workflow"
401 | - "Deactivate the old password reset workflow"
402 | - "Add an approval activity to the software license request workflow"
403 | - "Update the notification activity in the incident escalation workflow"
404 | - "Delete the unnecessary activity from the change request workflow"
405 | - "Reorder the activities in the service catalog request workflow"
406 | 
407 | #### Changeset Management Examples
408 | - "List all changesets in ServiceNow"
409 | - "Show me all changesets created by developer 'john.doe'"
410 | - "Get details about changeset 'sys_update_set_123'"
411 | - "Create a new changeset for the 'HR Portal' application"
412 | - "Update the description of changeset 'sys_update_set_123'"
413 | - "Commit changeset 'sys_update_set_123' with message 'Fixed login issue'"
414 | - "Publish changeset 'sys_update_set_123' to production"
415 | - "Add a file to changeset 'sys_update_set_123'"
416 | - "Show me all changes in changeset 'sys_update_set_123'"
417 | 
418 | #### Knowledge Base Examples
419 | - "Create a new knowledge base for the IT department"
420 | - "List all knowledge bases in the organization"
421 | - "Create a category called 'Network Troubleshooting' in the IT knowledge base"
422 | - "Write an article about VPN setup in the Network Troubleshooting category"
423 | - "Update the VPN setup article to include mobile device instructions"
424 | - "Publish the VPN setup article so it's visible to all users"
425 | - "List all articles in the Network Troubleshooting category"
426 | - "Show me the details of the VPN setup article"
427 | - "Find knowledge articles containing 'password reset' in the IT knowledge base"
428 | - "Create a subcategory called 'Wireless Networks' under the Network Troubleshooting category"
429 | 
430 | #### User Management Examples
431 | - "Create a new user Dr. Alice Radiology in the Radiology department"
432 | - "Update Bob's user record to make him the manager of Alice"
433 | - "Assign the ITIL role to Bob so he can approve change requests"
434 | - "List all users in the Radiology department"
435 | - "Create a new group called 'Biomedical Engineering' for managing medical devices"
436 | - "Add an admin user to the Biomedical Engineering group as a member"
437 | - "Update the Biomedical Engineering group to change its manager"
438 | - "Remove a user from the Biomedical Engineering group"
439 | - "Find all active users in the system with 'doctor' in their title"
440 | - "Create a user that will act as an approver for the Radiology department"
441 | - "List all IT support groups in the system"
442 | 
443 | #### UI Policy Examples
444 | - "Create a UI policy for the 'Software Request' item (sys_id: abc...) named 'Show Justification' that applies when 'software_cost' is greater than 100."
445 | - "For the UI policy 'Show Justification' (sys_id: def...), add an action to make the 'business_justification' variable visible and mandatory."
446 | - "Create another action for policy 'Show Justification' to hide the 'alternative_software' variable."
447 | 
448 | ### Example Scripts
449 | 
450 | The repository includes example scripts that demonstrate how to use the tools:
451 | 
452 | - **examples/catalog_optimization_example.py**: Demonstrates how to analyze and improve the ServiceNow Service Catalog
453 | - **examples/change_management_demo.py**: Shows how to create and manage change requests in ServiceNow
454 | 
455 | ## Authentication Methods
456 | 
457 | ### Basic Authentication
458 | 
459 | ```
460 | SERVICENOW_AUTH_TYPE=basic
461 | SERVICENOW_USERNAME=your-username
462 | SERVICENOW_PASSWORD=your-password
463 | ```
464 | 
465 | ### OAuth Authentication
466 | 
467 | ```
468 | SERVICENOW_AUTH_TYPE=oauth
469 | SERVICENOW_CLIENT_ID=your-client-id
470 | SERVICENOW_CLIENT_SECRET=your-client-secret
471 | SERVICENOW_TOKEN_URL=https://your-instance.service-now.com/oauth_token.do
472 | ```
473 | 
474 | ### API Key Authentication
475 | 
476 | ```
477 | SERVICENOW_AUTH_TYPE=api_key
478 | SERVICENOW_API_KEY=your-api-key
479 | ```
480 | 
481 | ## Development
482 | 
483 | ### Documentation
484 | 
485 | Additional documentation is available in the `docs` directory:
486 | 
487 | - [Catalog Integration](docs/catalog.md) - Detailed information about the Service Catalog integration
488 | - [Catalog Optimization](docs/catalog_optimization_plan.md) - Detailed plan for catalog optimization features
489 | - [Change Management](docs/change_management.md) - Detailed information about the Change Management tools
490 | - [Workflow Management](docs/workflow_management.md) - Detailed information about the Workflow Management tools
491 | - [Changeset Management](docs/changeset_management.md) - Detailed information about the Changeset Management tools
492 | 
493 | ### Troubleshooting
494 | 
495 | #### Common Errors with Change Management Tools
496 | 
497 | 1. **Error: `argument after ** must be a mapping, not CreateChangeRequestParams`**
498 |    - This error occurs when you pass a `CreateChangeRequestParams` object instead of a dictionary to the `create_change_request` function.
499 |    - Solution: Make sure you're passing a dictionary with the parameters, not a Pydantic model object.
500 |    - Note: The change management tools have been updated to handle this error automatically. The functions will now attempt to unwrap parameters if they're incorrectly wrapped or passed as a Pydantic model object.
501 | 
502 | 2. **Error: `Missing required parameter 'type'`**
503 |    - This error occurs when you don't provide all required parameters for creating a change request.
504 |    - Solution: Make sure to include all required parameters. For `create_change_request`, both `short_description` and `type` are required.
505 | 
506 | 3. **Error: `Invalid value for parameter 'type'`**
507 |    - This error occurs when you provide an invalid value for the `type` parameter.
508 |    - Solution: Use one of the valid values: "normal", "standard", or "emergency".
509 | 
510 | 4. **Error: `Cannot find get_headers method in either auth_manager or server_config`**
511 |    - This error occurs when the parameters are passed in the wrong order or when using objects that don't have the required methods.
512 |    - Solution: Make sure you're passing the `auth_manager` and `server_config` parameters in the correct order. The functions have been updated to handle parameter swapping automatically.
513 | 
514 | ### Contributing
515 | 
516 | Contributions are welcome! Please feel free to submit a Pull Request.
517 | 
518 | 1. Fork the repository
519 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
520 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
521 | 4. Push to the branch (`git push origin feature/amazing-feature`)
522 | 5. Open a Pull Request
523 | 
524 | ### License
525 | 
526 | This project is licensed under the MIT License - see the LICENSE file for details.
527 | 
```

--------------------------------------------------------------------------------
/src/servicenow_mcp/auth/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Authentication module for the ServiceNow MCP server.
3 | """
4 | 
5 | from servicenow_mcp.auth.auth_manager import AuthManager
6 | 
7 | __all__ = ["AuthManager"] 
```

--------------------------------------------------------------------------------
/src/servicenow_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | ServiceNow MCP Server
 3 | 
 4 | A Model Context Protocol (MCP) server implementation for ServiceNow,
 5 | focusing on the ITSM module.
 6 | """
 7 | 
 8 | __version__ = "0.1.0"
 9 | 
10 | from servicenow_mcp.server import ServiceNowMCP
11 | 
12 | __all__ = ["ServiceNowMCP"] 
```

--------------------------------------------------------------------------------
/src/servicenow_mcp/utils/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Utilities module for the ServiceNow MCP server.
 3 | """
 4 | 
 5 | from servicenow_mcp.utils.config import (
 6 |     ApiKeyConfig,
 7 |     AuthConfig,
 8 |     AuthType,
 9 |     BasicAuthConfig,
10 |     OAuthConfig,
11 |     ServerConfig,
12 | )
13 | 
14 | __all__ = [
15 |     "ApiKeyConfig",
16 |     "AuthConfig",
17 |     "AuthType",
18 |     "BasicAuthConfig",
19 |     "OAuthConfig",
20 |     "ServerConfig",
21 | ] 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM python:3.11-slim
 2 | 
 3 | WORKDIR /app
 4 | 
 5 | # Copy project files
 6 | COPY pyproject.toml README.md LICENSE ./
 7 | COPY src/ ./src/
 8 | 
 9 | # Install the package in development mode
10 | RUN pip install -e .
11 | 
12 | # Expose the port the app runs on
13 | EXPOSE 8080
14 | 
15 | # Command to run the application using the provided CLI
16 | CMD ["servicenow-mcp-sse", "--host=0.0.0.0", "--port=8080"] 
```

--------------------------------------------------------------------------------
/examples/claude_desktop_config.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "mcpServers": {
 3 |     "servicenow": {
 4 |       "command": "python",
 5 |       "args": [
 6 |         "-m",
 7 |         "servicenow_mcp.cli"
 8 |       ],
 9 |       "env": {
10 |         "SERVICENOW_INSTANCE_URL": "https://your-instance.service-now.com",
11 |         "SERVICENOW_USERNAME": "your-username",
12 |         "SERVICENOW_PASSWORD": "your-password",
13 |         "SERVICENOW_AUTH_TYPE": "basic"
14 |       }
15 |     }
16 |   }
17 | } 
```

--------------------------------------------------------------------------------
/src/servicenow_mcp/utils/config.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Configuration module for the ServiceNow MCP server.
 3 | """
 4 | 
 5 | from enum import Enum
 6 | from typing import Optional
 7 | 
 8 | from pydantic import BaseModel, Field
 9 | 
10 | 
11 | class AuthType(str, Enum):
12 |     """Authentication types supported by the ServiceNow MCP server."""
13 | 
14 |     BASIC = "basic"
15 |     OAUTH = "oauth"
16 |     API_KEY = "api_key"
17 | 
18 | 
19 | class BasicAuthConfig(BaseModel):
20 |     """Configuration for basic authentication."""
21 | 
22 |     username: str
23 |     password: str
24 | 
25 | 
26 | class OAuthConfig(BaseModel):
27 |     """Configuration for OAuth authentication."""
28 | 
29 |     client_id: str
30 |     client_secret: str
31 |     username: str
32 |     password: str
33 |     token_url: Optional[str] = None
34 | 
35 | 
36 | class ApiKeyConfig(BaseModel):
37 |     """Configuration for API key authentication."""
38 | 
39 |     api_key: str
40 |     header_name: str = "X-ServiceNow-API-Key"
41 | 
42 | 
43 | class AuthConfig(BaseModel):
44 |     """Authentication configuration."""
45 | 
46 |     type: AuthType
47 |     basic: Optional[BasicAuthConfig] = None
48 |     oauth: Optional[OAuthConfig] = None
49 |     api_key: Optional[ApiKeyConfig] = None
50 | 
51 | 
52 | class ServerConfig(BaseModel):
53 |     """Server configuration."""
54 | 
55 |     instance_url: str
56 |     auth: AuthConfig
57 |     debug: bool = False
58 |     timeout: int = 30
59 | 
60 |     @property
61 |     def api_url(self) -> str:
62 |         """Get the API URL for the ServiceNow instance."""
63 |         return f"{self.instance_url}/api/now"
64 | 
```

--------------------------------------------------------------------------------
/scripts/install_claude_desktop.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Check if Claude Desktop config file exists
 4 | CONFIG_FILE="$HOME/.config/claude/claude_desktop_config.json"
 5 | BACKUP_FILE="$HOME/.config/claude/claude_desktop_config.json.bak"
 6 | 
 7 | # Create config directory if it doesn't exist
 8 | mkdir -p "$HOME/.config/claude"
 9 | 
10 | # Backup existing config if it exists
11 | if [ -f "$CONFIG_FILE" ]; then
12 |     echo "Backing up existing Claude Desktop configuration..."
13 |     cp "$CONFIG_FILE" "$BACKUP_FILE"
14 | fi
15 | 
16 | # Get the absolute path to the current directory
17 | CURRENT_DIR=$(pwd)
18 | 
19 | # Create or update the Claude Desktop config
20 | echo "Creating Claude Desktop configuration..."
21 | cat > "$CONFIG_FILE" << EOL
22 | {
23 |   "mcpServers": {
24 |     "servicenow": {
25 |       "command": "$CURRENT_DIR/.venv/bin/python",
26 |       "args": [
27 |         "-m",
28 |         "servicenow_mcp.cli"
29 |       ],
30 |       "env": {
31 |         "SERVICENOW_INSTANCE_URL": "$(grep SERVICENOW_INSTANCE_URL .env | cut -d '=' -f2)",
32 |         "SERVICENOW_USERNAME": "$(grep SERVICENOW_USERNAME .env | cut -d '=' -f2)",
33 |         "SERVICENOW_PASSWORD": "$(grep SERVICENOW_PASSWORD .env | cut -d '=' -f2)",
34 |         "SERVICENOW_AUTH_TYPE": "$(grep SERVICENOW_AUTH_TYPE .env | head -1 | cut -d '=' -f2)"
35 |       }
36 |     }
37 |   }
38 | }
39 | EOL
40 | 
41 | echo "ServiceNow MCP server installed in Claude Desktop!"
42 | echo "You can now use it by opening Claude Desktop and selecting the ServiceNow MCP server."
43 | echo ""
44 | echo "If you need to update your ServiceNow credentials, edit the .env file and run this script again." 
```

--------------------------------------------------------------------------------
/scripts/setup.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Create directory if it doesn't exist
 4 | mkdir -p scripts
 5 | 
 6 | # Check if uv is installed
 7 | if ! command -v uv &> /dev/null; then
 8 |     echo "uv is not installed. Installing..."
 9 |     pip install uv
10 | fi
11 | 
12 | # Create virtual environment
13 | echo "Creating virtual environment..."
14 | uv venv .venv
15 | 
16 | # Activate virtual environment
17 | echo "Activating virtual environment..."
18 | source .venv/bin/activate
19 | 
20 | # Install dependencies
21 | echo "Installing dependencies..."
22 | uv pip install -e ".[dev]"
23 | 
24 | # Create .env file if it doesn't exist
25 | if [ ! -f .env ]; then
26 |     echo "Creating .env file..."
27 |     cat > .env << EOL
28 | # ServiceNow Instance Configuration
29 | SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com
30 | SERVICENOW_USERNAME=your-username
31 | SERVICENOW_PASSWORD=your-password
32 | 
33 | # OAuth Configuration (if using OAuth)
34 | SERVICENOW_AUTH_TYPE=basic
35 | # SERVICENOW_AUTH_TYPE=oauth
36 | # SERVICENOW_CLIENT_ID=your-client-id
37 | # SERVICENOW_CLIENT_SECRET=your-client-secret
38 | # SERVICENOW_TOKEN_URL=https://your-instance.service-now.com/oauth_token.do
39 | 
40 | # API Key Configuration (if using API Key)
41 | # SERVICENOW_AUTH_TYPE=api_key
42 | # SERVICENOW_API_KEY=your-api-key
43 | # SERVICENOW_API_KEY_HEADER=X-ServiceNow-API-Key
44 | 
45 | # Debug Configuration
46 | SERVICENOW_DEBUG=false
47 | SERVICENOW_TIMEOUT=30
48 | EOL
49 |     echo "Please update the .env file with your ServiceNow credentials."
50 | fi
51 | 
52 | echo "Setup complete! You can now run the server with:"
53 | echo "python examples/basic_server.py"
54 | echo ""
55 | echo "To use with Claude Desktop, copy the configuration from examples/claude_desktop_config.json to your Claude Desktop configuration." 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [build-system]
 2 | requires = ["hatchling"]
 3 | build-backend = "hatchling.build"
 4 | 
 5 | [project]
 6 | name = "servicenow-mcp"
 7 | version = "0.1.0"
 8 | description = "A Model Context Protocol (MCP) server implementation for ServiceNow"
 9 | readme = "README.md"
10 | requires-python = ">=3.11"
11 | license = {file = "LICENSE"}
12 | authors = [
13 |     {name = "ServiceNow MCP Contributors"},
14 | ]
15 | classifiers = [
16 |     "Development Status :: 3 - Alpha",
17 |     "Intended Audience :: Developers",
18 |     "License :: OSI Approved :: MIT License",
19 |     "Programming Language :: Python :: 3",
20 |     "Programming Language :: Python :: 3.11",
21 | ]
22 | dependencies = [
23 |     "mcp[cli]==1.3.0",
24 |     "requests>=2.28.0",
25 |     "pydantic>=2.0.0",
26 |     "python-dotenv>=1.0.0",
27 |     "starlette>=0.27.0",
28 |     "uvicorn>=0.22.0",
29 |     "httpx>=0.24.0",
30 |     "PyYAML>=6.0",
31 | ]
32 | 
33 | [project.optional-dependencies]
34 | dev = [
35 |     "pytest>=7.0.0",
36 |     "pytest-cov>=4.0.0",
37 |     "black>=23.0.0",
38 |     "isort>=5.12.0",
39 |     "mypy>=1.0.0",
40 |     "ruff>=0.0.1",
41 | ]
42 | 
43 | [project.scripts]
44 | servicenow-mcp = "servicenow_mcp.cli:main"
45 | servicenow-mcp-sse = "servicenow_mcp.server_sse:main"
46 | 
47 | [tool.hatch.build.targets.wheel]
48 | packages = ["src/servicenow_mcp"]
49 | 
50 | [tool.black]
51 | line-length = 100
52 | target-version = ["py311"]
53 | 
54 | [tool.isort]
55 | profile = "black"
56 | line_length = 100
57 | 
58 | [tool.mypy]
59 | python_version = "3.11"
60 | warn_return_any = true
61 | warn_unused_configs = true
62 | disallow_untyped_defs = true
63 | disallow_incomplete_defs = true
64 | 
65 | [tool.ruff]
66 | line-length = 100
67 | target-version = "py311"
68 | select = ["E", "F", "B", "I"]
69 | ignore = []
70 | 
71 | [tool.pytest.ini_options]
72 | testpaths = ["tests"]
73 | python_files = "test_*.py"
74 | python_classes = "Test*"
75 | python_functions = "test_*"
76 | addopts = "--ignore=examples" 
77 | 
```

--------------------------------------------------------------------------------
/tests/test_server_catalog_optimization.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Tests for the ServiceNow MCP server integration with catalog optimization tools.
 3 | """
 4 | 
 5 | import unittest
 6 | from unittest.mock import MagicMock, patch
 7 | 
 8 | from servicenow_mcp.auth.auth_manager import AuthManager
 9 | from servicenow_mcp.tools.catalog_optimization import (
10 |     OptimizationRecommendationsParams,
11 |     UpdateCatalogItemParams,
12 |     get_optimization_recommendations,
13 |     update_catalog_item,
14 | )
15 | from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
16 | 
17 | 
18 | class TestCatalogOptimizationToolParameters(unittest.TestCase):
19 |     """Test cases for the catalog optimization tool parameters."""
20 | 
21 |     def test_tool_parameter_classes(self):
22 |         """Test that the parameter classes for the tools are properly defined."""
23 |         # Test OptimizationRecommendationsParams
24 |         params = OptimizationRecommendationsParams(
25 |             recommendation_types=["inactive_items", "low_usage"],
26 |             category_id="hardware"
27 |         )
28 |         self.assertEqual(params.recommendation_types, ["inactive_items", "low_usage"])
29 |         self.assertEqual(params.category_id, "hardware")
30 | 
31 |         # Test with default values
32 |         params = OptimizationRecommendationsParams(
33 |             recommendation_types=["inactive_items"]
34 |         )
35 |         self.assertEqual(params.recommendation_types, ["inactive_items"])
36 |         self.assertIsNone(params.category_id)
37 | 
38 |         # Test UpdateCatalogItemParams
39 |         params = UpdateCatalogItemParams(
40 |             item_id="item1",
41 |             name="Updated Laptop",
42 |             short_description="High-performance laptop",
43 |             description="Detailed description of the laptop",
44 |             category="hardware",
45 |             price="1099.99",
46 |             active=True,
47 |             order=100
48 |         )
49 |         self.assertEqual(params.item_id, "item1")
50 |         self.assertEqual(params.name, "Updated Laptop")
51 |         self.assertEqual(params.short_description, "High-performance laptop")
52 |         self.assertEqual(params.description, "Detailed description of the laptop")
53 |         self.assertEqual(params.category, "hardware")
54 |         self.assertEqual(params.price, "1099.99")
55 |         self.assertTrue(params.active)
56 |         self.assertEqual(params.order, 100)
57 | 
58 |         # Test with only required parameters
59 |         params = UpdateCatalogItemParams(
60 |             item_id="item1"
61 |         )
62 |         self.assertEqual(params.item_id, "item1")
63 |         self.assertIsNone(params.name)
64 |         self.assertIsNone(params.short_description)
65 |         self.assertIsNone(params.description)
66 |         self.assertIsNone(params.category)
67 |         self.assertIsNone(params.price)
68 |         self.assertIsNone(params.active)
69 |         self.assertIsNone(params.order)
70 | 
71 | 
72 | if __name__ == "__main__":
73 |     unittest.main() 
```

--------------------------------------------------------------------------------
/tests/test_server_workflow.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Tests for the ServiceNow MCP server workflow management integration.
 3 | """
 4 | 
 5 | import unittest
 6 | from unittest.mock import MagicMock, patch
 7 | 
 8 | from servicenow_mcp.server import ServiceNowMCP
 9 | from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
10 | 
11 | 
12 | class TestServerWorkflow(unittest.TestCase):
13 |     """Tests for the ServiceNow MCP server workflow management integration."""
14 | 
15 |     def setUp(self):
16 |         """Set up test fixtures."""
17 |         self.auth_config = AuthConfig(
18 |             type=AuthType.BASIC,
19 |             basic=BasicAuthConfig(username="test_user", password="test_password"),
20 |         )
21 |         self.server_config = ServerConfig(
22 |             instance_url="https://test.service-now.com",
23 |             auth=self.auth_config,
24 |         )
25 |         
26 |         # Create a mock FastMCP instance
27 |         self.mock_mcp = MagicMock()
28 |         
29 |         # Patch the FastMCP class
30 |         self.patcher = patch("servicenow_mcp.server.FastMCP", return_value=self.mock_mcp)
31 |         self.mock_fastmcp = self.patcher.start()
32 |         
33 |         # Create the server instance
34 |         self.server = ServiceNowMCP(self.server_config)
35 |         
36 |     def tearDown(self):
37 |         """Tear down test fixtures."""
38 |         self.patcher.stop()
39 | 
40 |     def test_register_workflow_tools(self):
41 |         """Test that workflow tools are registered with the MCP server."""
42 |         # Get all the tool decorator calls
43 |         tool_decorator_calls = self.mock_mcp.tool.call_count
44 |         
45 |         # Verify that the tool decorator was called at least 12 times (for all workflow tools)
46 |         self.assertGreaterEqual(tool_decorator_calls, 12, 
47 |                                "Expected at least 12 tool registrations for workflow tools")
48 |         
49 |         # Check that the workflow tools are registered by examining the decorated functions
50 |         decorated_functions = []
51 |         for call in self.mock_mcp.tool.call_args_list:
52 |             # Each call to tool() returns a decorator function
53 |             decorator = call[0][0] if call[0] else call[1].get('return_value', None)
54 |             if decorator:
55 |                 decorated_functions.append(decorator.__name__)
56 |         
57 |         # Check for workflow tool registrations
58 |         workflow_tools = [
59 |             "list_workflows",
60 |             "get_workflow_details",
61 |             "list_workflow_versions",
62 |             "get_workflow_activities",
63 |             "create_workflow",
64 |             "update_workflow",
65 |             "activate_workflow",
66 |             "deactivate_workflow",
67 |             "add_workflow_activity",
68 |             "update_workflow_activity",
69 |             "delete_workflow_activity",
70 |             "reorder_workflow_activities",
71 |         ]
72 |         
73 |         # Print the decorated functions for debugging
74 |         print(f"Decorated functions: {decorated_functions}")
75 |         
76 |         # Check that all workflow tools are registered
77 |         for tool in workflow_tools:
78 |             self.assertIn(tool, str(self.mock_mcp.mock_calls), 
79 |                          f"Expected {tool} to be registered")
80 | 
81 | 
82 | if __name__ == "__main__":
83 |     unittest.main() 
```

--------------------------------------------------------------------------------
/examples/claude_incident_demo.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python
 2 | """
 3 | Claude Desktop Incident Management Demo
 4 | 
 5 | This script demonstrates how to use the ServiceNow MCP server with Claude Desktop
 6 | to manage incidents.
 7 | 
 8 | Prerequisites:
 9 | 1. Claude Desktop installed
10 | 2. ServiceNow MCP server configured in Claude Desktop
11 | 3. Valid ServiceNow credentials
12 | """
13 | 
14 | import json
15 | import os
16 | import subprocess
17 | import sys
18 | from pathlib import Path
19 | 
20 | from dotenv import load_dotenv
21 | 
22 | # Load environment variables
23 | load_dotenv()
24 | 
25 | # Get configuration from environment variables
26 | instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
27 | username = os.getenv("SERVICENOW_USERNAME")
28 | password = os.getenv("SERVICENOW_PASSWORD")
29 | 
30 | if not instance_url or not username or not password:
31 |     print("Error: Missing required environment variables.")
32 |     print("Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
33 |     sys.exit(1)
34 | 
35 | # Create Claude Desktop configuration
36 | claude_config = {
37 |     "mcpServers": {
38 |         "servicenow": {
39 |             "command": "python",
40 |             "args": [
41 |                 "-m",
42 |                 "servicenow_mcp.cli"
43 |             ],
44 |             "env": {
45 |                 "SERVICENOW_INSTANCE_URL": instance_url,
46 |                 "SERVICENOW_USERNAME": username,
47 |                 "SERVICENOW_PASSWORD": password,
48 |                 "SERVICENOW_AUTH_TYPE": "basic"
49 |             }
50 |         }
51 |     }
52 | }
53 | 
54 | # Save configuration to a temporary file
55 | config_path = Path.home() / ".claude-desktop" / "config.json"
56 | config_path.parent.mkdir(parents=True, exist_ok=True)
57 | 
58 | with open(config_path, "w") as f:
59 |     json.dump(claude_config, f, indent=2)
60 | 
61 | print(f"Claude Desktop configuration saved to {config_path}")
62 | print("You can now start Claude Desktop and use the following prompts:")
63 | 
64 | print("\n=== Example Prompts ===")
65 | print("\n1. List recent incidents:")
66 | print("   Can you list the 5 most recent incidents in ServiceNow?")
67 | 
68 | print("\n2. Get incident details:")
69 | print("   Can you show me the details of incident INC0010001?")
70 | 
71 | print("\n3. Create a new incident:")
72 | print("   Please create a new incident in ServiceNow with the following details:")
73 | print("   - Short description: Email service is down")
74 | print("   - Description: Users are unable to send or receive emails")
75 | print("   - Category: Software")
76 | print("   - Priority: 1")
77 | 
78 | print("\n4. Update an incident:")
79 | print("   Please update incident INC0010001 with the following changes:")
80 | print("   - Priority: 2")
81 | print("   - Assigned to: admin")
82 | print("   - Add work note: Investigating the issue")
83 | 
84 | print("\n5. Resolve an incident:")
85 | print("   Please resolve incident INC0010001 with the following details:")
86 | print("   - Resolution code: Solved (Permanently)")
87 | print("   - Resolution notes: The email service has been restored")
88 | 
89 | print("\n=== Starting Claude Desktop ===")
90 | print("Press Ctrl+C to exit this script and continue using Claude Desktop.")
91 | 
92 | try:
93 |     # Try to start Claude Desktop
94 |     subprocess.run(["claude"], check=True)
95 | except KeyboardInterrupt:
96 |     print("\nExiting script. Claude Desktop should be running.")
97 | except Exception as e:
98 |     print(f"\nFailed to start Claude Desktop: {e}")
99 |     print("Please start Claude Desktop manually.") 
```

--------------------------------------------------------------------------------
/tests/test_incident_tools.py:
--------------------------------------------------------------------------------

```python
 1 | 
 2 | import unittest
 3 | from unittest.mock import MagicMock, patch
 4 | from servicenow_mcp.tools.incident_tools import get_incident_by_number, GetIncidentByNumberParams
 5 | from servicenow_mcp.utils.config import ServerConfig, AuthConfig, AuthType, BasicAuthConfig
 6 | from servicenow_mcp.auth.auth_manager import AuthManager
 7 | 
 8 | class TestIncidentTools(unittest.TestCase):
 9 | 
10 |     def setUp(self):
11 |         self.auth_config = AuthConfig(type=AuthType.BASIC, basic=BasicAuthConfig(username='test', password='test'))
12 | 
13 |     @patch('requests.get')
14 |     def test_get_incident_by_number_success(self, mock_get):
15 |         # Mock the server configuration
16 |         config = ServerConfig(instance_url="https://dev12345.service-now.com", auth=self.auth_config)
17 | 
18 |         # Mock the authentication manager
19 |         auth_manager = MagicMock(spec=AuthManager)
20 |         auth_manager.get_headers.return_value = {"Authorization": "Bearer FAKE_TOKEN"}
21 | 
22 |         # Mock the requests.get call
23 |         mock_response = MagicMock()
24 |         mock_response.status_code = 200
25 |         mock_response.json.return_value = {
26 |             "result": [
27 |                 {
28 |                     "sys_id": "12345",
29 |                     "number": "INC0010001",
30 |                     "short_description": "Test incident",
31 |                     "description": "This is a test incident",
32 |                     "state": "New",
33 |                     "priority": "1 - Critical",
34 |                     "assigned_to": "John Doe",
35 |                     "category": "Software",
36 |                     "subcategory": "Email",
37 |                     "sys_created_on": "2025-06-25 10:00:00",
38 |                     "sys_updated_on": "2025-06-25 10:00:00"
39 |                 }
40 |             ]
41 |         }
42 |         mock_get.return_value = mock_response
43 | 
44 |         # Call the function with test data
45 |         params = GetIncidentByNumberParams(incident_number="INC0010001")
46 |         result = get_incident_by_number(config, auth_manager, params)
47 | 
48 |         # Assert the results
49 |         self.assertTrue(result["success"])
50 |         self.assertEqual(result["message"], "Incident INC0010001 found")
51 |         self.assertIn("incident", result)
52 |         self.assertEqual(result["incident"]["number"], "INC0010001")
53 | 
54 |     @patch('requests.get')
55 |     def test_get_incident_by_number_not_found(self, mock_get):
56 |         # Mock the server configuration
57 |         config = ServerConfig(instance_url="https://dev12345.service-now.com", auth=self.auth_config)
58 | 
59 |         # Mock the authentication manager
60 |         auth_manager = MagicMock(spec=AuthManager)
61 |         auth_manager.get_headers.return_value = {"Authorization": "Bearer FAKE_TOKEN"}
62 | 
63 |         # Mock the requests.get call for a not found scenario
64 |         mock_response = MagicMock()
65 |         mock_response.status_code = 200
66 |         mock_response.json.return_value = {"result": []}
67 |         mock_get.return_value = mock_response
68 | 
69 |         # Call the function with a non-existent incident number
70 |         params = GetIncidentByNumberParams(incident_number="INC9999999")
71 |         result = get_incident_by_number(config, auth_manager, params)
72 | 
73 |         # Assert the results
74 |         self.assertFalse(result["success"])
75 |         self.assertEqual(result["message"], "Incident not found: INC9999999")
76 | 
77 | if __name__ == '__main__':
78 |     unittest.main()
79 | 
```

--------------------------------------------------------------------------------
/examples/claude_catalog_demo.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python
  2 | """
  3 | Claude Desktop Service Catalog Demo
  4 | 
  5 | This script demonstrates how to use the ServiceNow MCP server with Claude Desktop
  6 | to interact with the ServiceNow Service Catalog.
  7 | 
  8 | Prerequisites:
  9 | 1. Claude Desktop installed
 10 | 2. ServiceNow MCP server configured in Claude Desktop
 11 | 3. Valid ServiceNow credentials with access to the Service Catalog
 12 | 
 13 | Usage:
 14 |     python examples/claude_catalog_demo.py [--dry-run]
 15 | """
 16 | 
 17 | import argparse
 18 | import json
 19 | import os
 20 | import subprocess
 21 | import sys
 22 | from pathlib import Path
 23 | 
 24 | from dotenv import load_dotenv
 25 | 
 26 | # Parse command line arguments
 27 | parser = argparse.ArgumentParser(description="Claude Desktop Service Catalog Demo")
 28 | parser.add_argument("--dry-run", action="store_true", help="Skip launching Claude Desktop")
 29 | args = parser.parse_args()
 30 | 
 31 | # Load environment variables
 32 | load_dotenv()
 33 | 
 34 | # Get configuration from environment variables
 35 | instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
 36 | username = os.getenv("SERVICENOW_USERNAME")
 37 | password = os.getenv("SERVICENOW_PASSWORD")
 38 | 
 39 | if not instance_url or not username or not password:
 40 |     print("Error: Missing required environment variables.")
 41 |     print("Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
 42 |     sys.exit(1)
 43 | 
 44 | # Create Claude Desktop configuration
 45 | claude_config = {
 46 |     "mcpServers": {
 47 |         "servicenow": {
 48 |             "command": "python",
 49 |             "args": ["-m", "servicenow_mcp.cli"],
 50 |             "env": {
 51 |                 "SERVICENOW_INSTANCE_URL": instance_url,
 52 |                 "SERVICENOW_USERNAME": username,
 53 |                 "SERVICENOW_PASSWORD": password,
 54 |                 "SERVICENOW_AUTH_TYPE": "basic",
 55 |             },
 56 |         }
 57 |     }
 58 | }
 59 | 
 60 | # Save configuration to a temporary file
 61 | config_path = Path.home() / ".claude-desktop" / "config.json"
 62 | config_path.parent.mkdir(parents=True, exist_ok=True)
 63 | 
 64 | with open(config_path, "w") as f:
 65 |     json.dump(claude_config, f, indent=2)
 66 | 
 67 | print(f"Claude Desktop configuration saved to {config_path}")
 68 | print("You can now start Claude Desktop and use the following prompts:")
 69 | 
 70 | print("\n=== Example Prompts for Service Catalog ===")
 71 | print("\n1. List catalog categories:")
 72 | print("   Can you list the available service catalog categories in ServiceNow?")
 73 | 
 74 | print("\n2. List catalog items:")
 75 | print("   Can you show me the available items in the ServiceNow service catalog?")
 76 | 
 77 | print("\n3. List items in a specific category:")
 78 | print("   Can you list the catalog items in the Hardware category?")
 79 | 
 80 | print("\n4. Get catalog item details:")
 81 | print("   Can you show me the details of the 'New Laptop' catalog item?")
 82 | 
 83 | print("\n5. Find items by keyword:")
 84 | print("   Can you find catalog items related to 'software' in ServiceNow?")
 85 | 
 86 | print("\n6. Compare catalog items:")
 87 | print("   Can you compare the different laptop options available in the service catalog?")
 88 | 
 89 | print("\n7. Explain catalog item variables:")
 90 | print("   What information do I need to provide when ordering a new laptop?")
 91 | 
 92 | if args.dry_run:
 93 |     print("\n=== Dry Run Mode ===")
 94 |     print("Skipping Claude Desktop launch. Start Claude Desktop manually to use the configuration.")
 95 |     sys.exit(0)
 96 | 
 97 | print("\n=== Starting Claude Desktop ===")
 98 | print("Press Ctrl+C to exit this script and continue using Claude Desktop.")
 99 | 
100 | try:
101 |     # Try to start Claude Desktop
102 |     subprocess.run(["claude"], check=True)
103 | except KeyboardInterrupt:
104 |     print("\nExiting script. Claude Desktop should be running.")
105 | except Exception as e:
106 |     print(f"\nFailed to start Claude Desktop: {e}")
107 |     print("Please start Claude Desktop manually.")
108 | 
```

--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the configuration module.
  3 | """
  4 | 
  5 | from servicenow_mcp.utils.config import (
  6 |     ApiKeyConfig,
  7 |     AuthConfig,
  8 |     AuthType,
  9 |     BasicAuthConfig,
 10 |     OAuthConfig,
 11 |     ServerConfig,
 12 | )
 13 | 
 14 | 
 15 | def test_auth_type_enum():
 16 |     """Test the AuthType enum."""
 17 |     assert AuthType.BASIC == "basic"
 18 |     assert AuthType.OAUTH == "oauth"
 19 |     assert AuthType.API_KEY == "api_key"
 20 | 
 21 | 
 22 | def test_basic_auth_config():
 23 |     """Test the BasicAuthConfig class."""
 24 |     config = BasicAuthConfig(username="user", password="pass")
 25 |     assert config.username == "user"
 26 |     assert config.password == "pass"
 27 | 
 28 | 
 29 | def test_oauth_config():
 30 |     """Test the OAuthConfig class."""
 31 |     config = OAuthConfig(
 32 |         client_id="client_id",
 33 |         client_secret="client_secret",
 34 |         username="user",
 35 |         password="pass",
 36 |     )
 37 |     assert config.client_id == "client_id"
 38 |     assert config.client_secret == "client_secret"
 39 |     assert config.username == "user"
 40 |     assert config.password == "pass"
 41 |     assert config.token_url is None
 42 |     
 43 |     config = OAuthConfig(
 44 |         client_id="client_id",
 45 |         client_secret="client_secret",
 46 |         username="user",
 47 |         password="pass",
 48 |         token_url="https://example.com/token",
 49 |     )
 50 |     assert config.token_url == "https://example.com/token"
 51 | 
 52 | 
 53 | def test_api_key_config():
 54 |     """Test the ApiKeyConfig class."""
 55 |     config = ApiKeyConfig(api_key="api_key")
 56 |     assert config.api_key == "api_key"
 57 |     assert config.header_name == "X-ServiceNow-API-Key"
 58 |     
 59 |     config = ApiKeyConfig(api_key="api_key", header_name="Custom-Header")
 60 |     assert config.header_name == "Custom-Header"
 61 | 
 62 | 
 63 | def test_auth_config():
 64 |     """Test the AuthConfig class."""
 65 |     # Basic auth
 66 |     config = AuthConfig(
 67 |         type=AuthType.BASIC,
 68 |         basic=BasicAuthConfig(username="user", password="pass"),
 69 |     )
 70 |     assert config.type == AuthType.BASIC
 71 |     assert config.basic is not None
 72 |     assert config.basic.username == "user"
 73 |     assert config.basic.password == "pass"
 74 |     assert config.oauth is None
 75 |     assert config.api_key is None
 76 |     
 77 |     # OAuth
 78 |     config = AuthConfig(
 79 |         type=AuthType.OAUTH,
 80 |         oauth=OAuthConfig(
 81 |             client_id="client_id",
 82 |             client_secret="client_secret",
 83 |             username="user",
 84 |             password="pass",
 85 |         ),
 86 |     )
 87 |     assert config.type == AuthType.OAUTH
 88 |     assert config.oauth is not None
 89 |     assert config.oauth.client_id == "client_id"
 90 |     assert config.basic is None
 91 |     assert config.api_key is None
 92 |     
 93 |     # API key
 94 |     config = AuthConfig(
 95 |         type=AuthType.API_KEY,
 96 |         api_key=ApiKeyConfig(api_key="api_key"),
 97 |     )
 98 |     assert config.type == AuthType.API_KEY
 99 |     assert config.api_key is not None
100 |     assert config.api_key.api_key == "api_key"
101 |     assert config.basic is None
102 |     assert config.oauth is None
103 | 
104 | 
105 | def test_server_config():
106 |     """Test the ServerConfig class."""
107 |     config = ServerConfig(
108 |         instance_url="https://example.service-now.com",
109 |         auth=AuthConfig(
110 |             type=AuthType.BASIC,
111 |             basic=BasicAuthConfig(username="user", password="pass"),
112 |         ),
113 |     )
114 |     assert config.instance_url == "https://example.service-now.com"
115 |     assert config.auth.type == AuthType.BASIC
116 |     assert config.debug is False
117 |     assert config.timeout == 30
118 |     assert config.api_url == "https://example.service-now.com/api/now"
119 |     
120 |     config = ServerConfig(
121 |         instance_url="https://example.service-now.com",
122 |         auth=AuthConfig(
123 |             type=AuthType.BASIC,
124 |             basic=BasicAuthConfig(username="user", password="pass"),
125 |         ),
126 |         debug=True,
127 |         timeout=60,
128 |     )
129 |     assert config.debug is True
130 |     assert config.timeout == 60 
```

--------------------------------------------------------------------------------
/scripts/setup_api_key.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python
 2 | """
 3 | ServiceNow API Key Setup Script
 4 | 
 5 | This script helps set up and test API key authentication with ServiceNow.
 6 | It will:
 7 | 1. Test the API key with a simple API call
 8 | 2. Update the .env file with the API key configuration
 9 | 
10 | Usage:
11 |     python scripts/setup_api_key.py
12 | """
13 | 
14 | import os
15 | import sys
16 | import requests
17 | from pathlib import Path
18 | from dotenv import load_dotenv
19 | 
20 | # Add the project root to the Python path
21 | sys.path.insert(0, str(Path(__file__).parent.parent))
22 | 
23 | def setup_api_key():
24 |     # Load environment variables
25 |     load_dotenv()
26 |     
27 |     # Get ServiceNow instance URL
28 |     instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
29 |     if not instance_url or instance_url == "https://your-instance.service-now.com":
30 |         instance_url = input("Enter your ServiceNow instance URL (e.g., https://dev296866.service-now.com): ")
31 |     
32 |     print("\n=== ServiceNow API Key Setup ===")
33 |     print("This script will help you set up API key authentication for your ServiceNow instance.")
34 |     print("You'll need to create an API key in ServiceNow first.")
35 |     print("\nTo create an API key in a Personal Developer Instance (PDI):")
36 |     print("1. Log in to your ServiceNow instance")
37 |     print("2. Navigate to User Profile > REST API Keys")
38 |     print("3. Click 'New'")
39 |     print("4. Fill in the required fields and save")
40 |     print("5. Copy the API key (you'll only see it once)")
41 |     
42 |     # Get API key
43 |     api_key = input("\nEnter your API key: ")
44 |     api_key_header = input("Enter the API key header name (default: X-ServiceNow-API-Key): ") or "X-ServiceNow-API-Key"
45 |     
46 |     print(f"\nTesting API key connection to {instance_url}...")
47 |     
48 |     # Test the API key
49 |     try:
50 |         # Make a test request
51 |         test_url = f"{instance_url}/api/now/table/incident?sysparm_limit=1"
52 |         test_response = requests.get(
53 |             test_url,
54 |             headers={
55 |                 api_key_header: api_key,
56 |                 'Accept': 'application/json'
57 |             }
58 |         )
59 |         
60 |         if test_response.status_code == 200:
61 |             print("✅ Successfully tested API key with API call!")
62 |             data = test_response.json()
63 |             print(f"Retrieved {len(data.get('result', []))} incident(s)")
64 |             
65 |             # Update .env file
66 |             update_env = input("\nDo you want to update your .env file with this API key? (y/n): ")
67 |             if update_env.lower() == 'y':
68 |                 env_path = Path(__file__).parent.parent / '.env'
69 |                 with open(env_path, 'r') as f:
70 |                     env_content = f.read()
71 |                 
72 |                 # Update API key configuration
73 |                 env_content = env_content.replace('SERVICENOW_AUTH_TYPE=basic', 'SERVICENOW_AUTH_TYPE=api_key')
74 |                 env_content = env_content.replace('# SERVICENOW_API_KEY=your-api-key', f'SERVICENOW_API_KEY={api_key}')
75 |                 env_content = env_content.replace('# SERVICENOW_API_KEY_HEADER=X-ServiceNow-API-Key', f'SERVICENOW_API_KEY_HEADER={api_key_header}')
76 |                 
77 |                 with open(env_path, 'w') as f:
78 |                     f.write(env_content)
79 |                 
80 |                 print("✅ Updated .env file with API key configuration!")
81 |                 print("\nYou can now use API key authentication with the ServiceNow MCP server.")
82 |                 print("To test it, run: python scripts/test_connection.py")
83 |             
84 |             return True
85 |         else:
86 |             print(f"❌ Failed to test API key with API call: {test_response.status_code}")
87 |             print(f"Response: {test_response.text}")
88 |             return False
89 |             
90 |     except requests.exceptions.RequestException as e:
91 |         print(f"❌ Connection error: {e}")
92 |         return False
93 | 
94 | if __name__ == "__main__":
95 |     setup_api_key() 
```

--------------------------------------------------------------------------------
/scripts/setup_auth.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python
  2 | """
  3 | ServiceNow Authentication Setup Menu
  4 | 
  5 | This script provides a menu to help users set up different authentication methods
  6 | for the ServiceNow MCP server.
  7 | 
  8 | Usage:
  9 |     python scripts/setup_auth.py
 10 | """
 11 | 
 12 | import os
 13 | import sys
 14 | import subprocess
 15 | from pathlib import Path
 16 | 
 17 | def clear_screen():
 18 |     """Clear the terminal screen."""
 19 |     os.system('cls' if os.name == 'nt' else 'clear')
 20 | 
 21 | def print_header():
 22 |     """Print the header for the menu."""
 23 |     print("=" * 60)
 24 |     print("ServiceNow MCP Server - Authentication Setup".center(60))
 25 |     print("=" * 60)
 26 |     print("\nThis script will help you set up authentication for your ServiceNow instance.")
 27 |     print("Choose one of the following authentication methods:\n")
 28 | 
 29 | def print_menu():
 30 |     """Print the menu options."""
 31 |     print("1. Basic Authentication (username/password)")
 32 |     print("2. OAuth Authentication (client ID/client secret)")
 33 |     print("3. API Key Authentication")
 34 |     print("4. Test Current Configuration")
 35 |     print("5. Exit")
 36 |     print("\nEnter your choice (1-5): ", end="")
 37 | 
 38 | def setup_basic_auth():
 39 |     """Set up basic authentication."""
 40 |     clear_screen()
 41 |     print("=" * 60)
 42 |     print("Basic Authentication Setup".center(60))
 43 |     print("=" * 60)
 44 |     print("\nYou'll need your ServiceNow instance URL, username, and password.")
 45 |     
 46 |     instance_url = input("\nEnter your ServiceNow instance URL: ")
 47 |     username = input("Enter your ServiceNow username: ")
 48 |     password = input("Enter your ServiceNow password: ")
 49 |     
 50 |     # Update .env file
 51 |     env_path = Path(__file__).parent.parent / '.env'
 52 |     with open(env_path, 'r') as f:
 53 |         env_content = f.read()
 54 |     
 55 |     # Update basic authentication configuration
 56 |     env_content = env_content.replace('SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com', f'SERVICENOW_INSTANCE_URL={instance_url}')
 57 |     env_content = env_content.replace('SERVICENOW_USERNAME=your-username', f'SERVICENOW_USERNAME={username}')
 58 |     env_content = env_content.replace('SERVICENOW_PASSWORD=your-password', f'SERVICENOW_PASSWORD={password}')
 59 |     
 60 |     # Ensure auth type is set to basic
 61 |     if 'SERVICENOW_AUTH_TYPE=oauth' in env_content:
 62 |         env_content = env_content.replace('SERVICENOW_AUTH_TYPE=oauth', 'SERVICENOW_AUTH_TYPE=basic')
 63 |     elif 'SERVICENOW_AUTH_TYPE=api_key' in env_content:
 64 |         env_content = env_content.replace('SERVICENOW_AUTH_TYPE=api_key', 'SERVICENOW_AUTH_TYPE=basic')
 65 |     
 66 |     with open(env_path, 'w') as f:
 67 |         f.write(env_content)
 68 |     
 69 |     print("\n✅ Updated .env file with basic authentication configuration!")
 70 |     input("\nPress Enter to continue...")
 71 | 
 72 | def main():
 73 |     """Main function to run the menu."""
 74 |     while True:
 75 |         clear_screen()
 76 |         print_header()
 77 |         print_menu()
 78 |         
 79 |         choice = input()
 80 |         
 81 |         if choice == '1':
 82 |             setup_basic_auth()
 83 |         elif choice == '2':
 84 |             # Run the OAuth setup script
 85 |             subprocess.run([sys.executable, str(Path(__file__).parent / 'setup_oauth.py')])
 86 |             input("\nPress Enter to continue...")
 87 |         elif choice == '3':
 88 |             # Run the API key setup script
 89 |             subprocess.run([sys.executable, str(Path(__file__).parent / 'setup_api_key.py')])
 90 |             input("\nPress Enter to continue...")
 91 |         elif choice == '4':
 92 |             # Run the test connection script
 93 |             clear_screen()
 94 |             print("Testing current configuration...\n")
 95 |             subprocess.run([sys.executable, str(Path(__file__).parent / 'test_connection.py')])
 96 |             input("\nPress Enter to continue...")
 97 |         elif choice == '5':
 98 |             clear_screen()
 99 |             print("Exiting...")
100 |             break
101 |         else:
102 |             print("Invalid choice. Please try again.")
103 |             input("\nPress Enter to continue...")
104 | 
105 | if __name__ == "__main__":
106 |     main() 
```

--------------------------------------------------------------------------------
/examples/wake_servicenow_instance.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python
  2 | """
  3 | Wake ServiceNow Instance
  4 | 
  5 | This script attempts to wake up a hibernating ServiceNow instance by
  6 | making requests to it and following any redirects to the wake-up page.
  7 | """
  8 | 
  9 | import os
 10 | import sys
 11 | import time
 12 | import logging
 13 | import requests
 14 | from dotenv import load_dotenv
 15 | 
 16 | # Set up logging
 17 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 18 | logger = logging.getLogger(__name__)
 19 | 
 20 | def wake_instance(instance_url, max_attempts=5, wait_time=10):
 21 |     """
 22 |     Attempt to wake up a hibernating ServiceNow instance.
 23 |     
 24 |     Args:
 25 |         instance_url: The URL of the ServiceNow instance
 26 |         max_attempts: Maximum number of wake-up attempts
 27 |         wait_time: Time to wait between attempts (seconds)
 28 |         
 29 |     Returns:
 30 |         bool: True if the instance appears to be awake, False otherwise
 31 |     """
 32 |     logger.info(f"Attempting to wake up ServiceNow instance: {instance_url}")
 33 |     
 34 |     # Create a session to handle cookies and redirects
 35 |     session = requests.Session()
 36 |     
 37 |     for attempt in range(1, max_attempts + 1):
 38 |         logger.info(f"Wake-up attempt {attempt}/{max_attempts}...")
 39 |         
 40 |         try:
 41 |             # Make a request to the instance
 42 |             response = session.get(
 43 |                 instance_url,
 44 |                 allow_redirects=True,
 45 |                 timeout=30
 46 |             )
 47 |             
 48 |             # Check if we got a JSON response from the API
 49 |             if "application/json" in response.headers.get("Content-Type", ""):
 50 |                 logger.info("Instance appears to be awake (JSON response received)")
 51 |                 return True
 52 |                 
 53 |             # Check if we're still getting the hibernation page
 54 |             if "Instance Hibernating" in response.text:
 55 |                 logger.info("Instance is still hibernating")
 56 |                 
 57 |                 # Look for the wake-up URL in the page
 58 |                 if "wu=true" in response.text:
 59 |                     wake_url = "https://developer.servicenow.com/dev.do#!/home?wu=true"
 60 |                     logger.info(f"Following wake-up URL: {wake_url}")
 61 |                     
 62 |                     # Make a request to the wake-up URL
 63 |                     wake_response = session.get(wake_url, allow_redirects=True, timeout=30)
 64 |                     logger.info(f"Wake-up request status: {wake_response.status_code}")
 65 |             else:
 66 |                 # Check if we got a login page or something else
 67 |                 logger.info(f"Got response with status {response.status_code}, but not the hibernation page")
 68 |                 logger.info(f"Content type: {response.headers.get('Content-Type')}")
 69 |                 
 70 |             # Wait before the next attempt
 71 |             if attempt < max_attempts:
 72 |                 logger.info(f"Waiting {wait_time} seconds before next attempt...")
 73 |                 time.sleep(wait_time)
 74 |                 
 75 |         except requests.RequestException as e:
 76 |             logger.error(f"Error during wake-up attempt: {e}")
 77 |             
 78 |             if attempt < max_attempts:
 79 |                 logger.info(f"Waiting {wait_time} seconds before next attempt...")
 80 |                 time.sleep(wait_time)
 81 |     
 82 |     logger.warning(f"Failed to wake up instance after {max_attempts} attempts")
 83 |     return False
 84 | 
 85 | def main():
 86 |     """Main function."""
 87 |     # Load environment variables
 88 |     load_dotenv()
 89 |     
 90 |     # Get ServiceNow instance URL
 91 |     instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
 92 |     if not instance_url:
 93 |         logger.error("SERVICENOW_INSTANCE_URL environment variable is not set")
 94 |         sys.exit(1)
 95 |     
 96 |     # Try to wake up the instance
 97 |     success = wake_instance(instance_url)
 98 |     
 99 |     if success:
100 |         logger.info("ServiceNow instance wake-up process completed successfully")
101 |         sys.exit(0)
102 |     else:
103 |         logger.error("Failed to wake up ServiceNow instance")
104 |         logger.info("You may need to manually wake up the instance by visiting:")
105 |         logger.info("https://developer.servicenow.com/dev.do#!/home?wu=true")
106 |         sys.exit(1)
107 | 
108 | if __name__ == "__main__":
109 |     main() 
```

--------------------------------------------------------------------------------
/scripts/check_pdi_status.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python
  2 | """
  3 | ServiceNow PDI Status Checker
  4 | 
  5 | This script checks the status of a ServiceNow PDI instance.
  6 | It will:
  7 | 1. Check if the instance is reachable
  8 | 2. Check if the instance is active or hibernating
  9 | 3. Provide guidance on waking up a hibernating instance
 10 | 
 11 | Usage:
 12 |     python scripts/check_pdi_status.py
 13 | """
 14 | 
 15 | import os
 16 | import sys
 17 | import requests
 18 | from pathlib import Path
 19 | from dotenv import load_dotenv
 20 | 
 21 | # Add the project root to the Python path
 22 | sys.path.insert(0, str(Path(__file__).parent.parent))
 23 | 
 24 | def check_instance_status(instance_url):
 25 |     """Check the status of a ServiceNow instance."""
 26 |     print(f"\nChecking instance status: {instance_url}")
 27 |     
 28 |     # Check if the instance is reachable
 29 |     try:
 30 |         # Try accessing the login page
 31 |         login_response = requests.get(f"{instance_url}/login.do", 
 32 |                                      allow_redirects=True,
 33 |                                      timeout=10)
 34 |         
 35 |         # Try accessing the API
 36 |         api_response = requests.get(f"{instance_url}/api/now/table/sys_properties?sysparm_limit=1", 
 37 |                                    headers={"Accept": "application/json"},
 38 |                                    timeout=10)
 39 |         
 40 |         # Check if the instance is hibernating
 41 |         if "instance is hibernating" in login_response.text.lower() or "instance is hibernating" in api_response.text.lower():
 42 |             print("❌ Instance is HIBERNATING")
 43 |             print("\nYour PDI is currently hibernating. To wake it up:")
 44 |             print("1. Go to https://developer.servicenow.com/")
 45 |             print("2. Log in with your ServiceNow account")
 46 |             print("3. Go to 'My Instances'")
 47 |             print("4. Find your PDI and click 'Wake'")
 48 |             print("5. Wait a few minutes for the instance to wake up")
 49 |             return False
 50 |         
 51 |         # Check if the instance is accessible
 52 |         if login_response.status_code == 200 and "ServiceNow" in login_response.text:
 53 |             print("✅ Instance is ACTIVE and accessible")
 54 |             print("✅ Login page is available")
 55 |             
 56 |             # Extract the instance name from the login page
 57 |             if "instance_name" in login_response.text:
 58 |                 start_index = login_response.text.find("instance_name")
 59 |                 end_index = login_response.text.find(";", start_index)
 60 |                 if start_index > 0 and end_index > start_index:
 61 |                     instance_info = login_response.text[start_index:end_index]
 62 |                     print(f"Instance info: {instance_info}")
 63 |             
 64 |             return True
 65 |         else:
 66 |             print(f"❌ Instance returned unexpected status code: {login_response.status_code}")
 67 |             print("❌ Login page may not be accessible")
 68 |             return False
 69 |             
 70 |     except requests.exceptions.RequestException as e:
 71 |         print(f"❌ Error connecting to instance: {e}")
 72 |         return False
 73 | 
 74 | def main():
 75 |     """Main function to run the PDI status checker."""
 76 |     load_dotenv()
 77 |     
 78 |     print("=" * 60)
 79 |     print("ServiceNow PDI Status Checker".center(60))
 80 |     print("=" * 60)
 81 |     
 82 |     # Get instance URL
 83 |     instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
 84 |     if not instance_url or instance_url == "https://your-instance.service-now.com":
 85 |         instance_url = input("Enter your ServiceNow instance URL: ")
 86 |     
 87 |     # Check instance status
 88 |     is_active = check_instance_status(instance_url)
 89 |     
 90 |     if is_active:
 91 |         print("\nYour PDI is active. To find your credentials:")
 92 |         print("1. Go to https://developer.servicenow.com/")
 93 |         print("2. Log in with your ServiceNow account")
 94 |         print("3. Go to 'My Instances'")
 95 |         print("4. Find your PDI and click on it")
 96 |         print("5. Look for the credentials information")
 97 |         
 98 |         print("\nDefault PDI credentials are usually:")
 99 |         print("Username: admin")
100 |         print("Password: (check on the developer portal)")
101 |     else:
102 |         print("\nPlease check your instance status on the ServiceNow Developer Portal.")
103 |         print("If your instance is hibernating, you'll need to wake it up before you can connect.")
104 | 
105 | if __name__ == "__main__":
106 |     main() 
```

--------------------------------------------------------------------------------
/src/servicenow_mcp/server_sse.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | ServiceNow MCP Server
  3 | 
  4 | This module provides the main implementation of the ServiceNow MCP server.
  5 | """
  6 | 
  7 | import argparse
  8 | import os
  9 | from typing import Dict, Union
 10 | 
 11 | import uvicorn
 12 | from dotenv import load_dotenv
 13 | from mcp.server import Server
 14 | from mcp.server.fastmcp import FastMCP
 15 | from mcp.server.sse import SseServerTransport
 16 | from starlette.applications import Starlette
 17 | from starlette.requests import Request
 18 | from starlette.routing import Mount, Route
 19 | 
 20 | from servicenow_mcp.server import ServiceNowMCP
 21 | from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
 22 | 
 23 | 
 24 | def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette:
 25 |     """Create a Starlette application that can serve the provided mcp server with SSE."""
 26 |     sse = SseServerTransport("/messages/")
 27 | 
 28 |     async def handle_sse(request: Request) -> None:
 29 |         async with sse.connect_sse(
 30 |             request.scope,
 31 |             request.receive,
 32 |             request._send,  # noqa: SLF001
 33 |         ) as (read_stream, write_stream):
 34 |             await mcp_server.run(
 35 |                 read_stream,
 36 |                 write_stream,
 37 |                 mcp_server.create_initialization_options(),
 38 |             )
 39 | 
 40 |     return Starlette(
 41 |         debug=debug,
 42 |         routes=[
 43 |             Route("/sse", endpoint=handle_sse),
 44 |             Mount("/messages/", app=sse.handle_post_message),
 45 |         ],
 46 |     )
 47 | 
 48 | 
 49 | class ServiceNowSSEMCP(ServiceNowMCP):
 50 |     """
 51 |     ServiceNow MCP Server implementation.
 52 | 
 53 |     This class provides a Model Context Protocol (MCP) server for ServiceNow,
 54 |     allowing LLMs to interact with ServiceNow data and functionality.
 55 |     """
 56 | 
 57 |     def __init__(self, config: Union[Dict, ServerConfig]):
 58 |         """
 59 |         Initialize the ServiceNow MCP server.
 60 | 
 61 |         Args:
 62 |             config: Server configuration, either as a dictionary or ServerConfig object.
 63 |         """
 64 |         super().__init__(config)
 65 | 
 66 |     def start(self, host: str = "0.0.0.0", port: int = 8080):
 67 |         """
 68 |         Start the MCP server with SSE transport using Starlette and Uvicorn.
 69 | 
 70 |         Args:
 71 |             host: Host address to bind to
 72 |             port: Port to listen on
 73 |         """
 74 |         # Create Starlette app with SSE transport
 75 |         starlette_app = create_starlette_app(self.mcp_server, debug=True)
 76 | 
 77 |         # Run using uvicorn
 78 |         uvicorn.run(starlette_app, host=host, port=port)
 79 | 
 80 | 
 81 | def create_servicenow_mcp(instance_url: str, username: str, password: str):
 82 |     """
 83 |     Create a ServiceNow MCP server with minimal configuration.
 84 | 
 85 |     This is a simplified factory function that creates a pre-configured
 86 |     ServiceNow MCP server with basic authentication.
 87 | 
 88 |     Args:
 89 |         instance_url: ServiceNow instance URL
 90 |         username: ServiceNow username
 91 |         password: ServiceNow password
 92 | 
 93 |     Returns:
 94 |         A configured ServiceNowMCP instance ready to use
 95 | 
 96 |     Example:
 97 |         ```python
 98 |         from servicenow_mcp.server import create_servicenow_mcp
 99 | 
100 |         # Create an MCP server for ServiceNow
101 |         mcp = create_servicenow_mcp(
102 |             instance_url="https://instance.service-now.com",
103 |             username="admin",
104 |             password="password"
105 |         )
106 | 
107 |         # Start the server
108 |         mcp.start()
109 |         ```
110 |     """
111 | 
112 |     # Create basic auth config
113 |     auth_config = AuthConfig(
114 |         type=AuthType.BASIC, basic=BasicAuthConfig(username=username, password=password)
115 |     )
116 | 
117 |     # Create server config
118 |     config = ServerConfig(instance_url=instance_url, auth=auth_config)
119 | 
120 |     # Create and return server
121 |     return ServiceNowSSEMCP(config)
122 | 
123 | 
124 | def main():
125 |     load_dotenv()
126 | 
127 |     # Parse command line arguments
128 |     parser = argparse.ArgumentParser(description="Run ServiceNow MCP SSE-based server")
129 |     parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
130 |     parser.add_argument("--port", type=int, default=8080, help="Port to listen on")
131 |     args = parser.parse_args()
132 | 
133 |     server = create_servicenow_mcp(
134 |         instance_url=os.getenv("SERVICENOW_INSTANCE_URL"),
135 |         username=os.getenv("SERVICENOW_USERNAME"),
136 |         password=os.getenv("SERVICENOW_PASSWORD"),
137 |     )
138 |     server.start(host=args.host, port=args.port)
139 | 
140 | 
141 | if __name__ == "__main__":
142 |     main()
143 | 
```

--------------------------------------------------------------------------------
/tests/test_server_catalog.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the ServiceNow MCP server integration with catalog functionality.
  3 | """
  4 | 
  5 | import unittest
  6 | from unittest.mock import MagicMock, patch
  7 | 
  8 | from servicenow_mcp.server import ServiceNowMCP
  9 | from servicenow_mcp.tools.catalog_tools import (
 10 |     GetCatalogItemParams,
 11 |     ListCatalogCategoriesParams,
 12 |     ListCatalogItemsParams,
 13 | )
 14 | from servicenow_mcp.tools.catalog_tools import (
 15 |     get_catalog_item as get_catalog_item_tool,
 16 | )
 17 | from servicenow_mcp.tools.catalog_tools import (
 18 |     list_catalog_categories as list_catalog_categories_tool,
 19 | )
 20 | from servicenow_mcp.tools.catalog_tools import (
 21 |     list_catalog_items as list_catalog_items_tool,
 22 | )
 23 | 
 24 | 
 25 | class TestServerCatalog(unittest.TestCase):
 26 |     """Test cases for the server integration with catalog functionality."""
 27 | 
 28 |     def setUp(self):
 29 |         """Set up test fixtures."""
 30 |         # Create a mock config
 31 |         self.config = {
 32 |             "instance_url": "https://example.service-now.com",
 33 |             "auth": {
 34 |                 "type": "basic",
 35 |                 "basic": {
 36 |                     "username": "admin",
 37 |                     "password": "password",
 38 |                 },
 39 |             },
 40 |         }
 41 | 
 42 |         # Create a mock server
 43 |         self.server = ServiceNowMCP(self.config)
 44 | 
 45 |         # Mock the FastMCP server
 46 |         self.server.mcp_server = MagicMock()
 47 |         self.server.mcp_server.resource = MagicMock()
 48 |         self.server.mcp_server.tool = MagicMock()
 49 | 
 50 |     def test_register_catalog_resources(self):
 51 |         """Test that catalog resources are registered correctly."""
 52 |         # Call the method to register resources
 53 |         self.server._register_resources()
 54 | 
 55 |         # Check that the resource decorators were called
 56 |         resource_calls = self.server.mcp_server.resource.call_args_list
 57 |         resource_paths = [call[0][0] for call in resource_calls]
 58 | 
 59 |         # Check that catalog resources are registered
 60 |         self.assertIn("catalog://items", resource_paths)
 61 |         self.assertIn("catalog://categories", resource_paths)
 62 |         self.assertIn("catalog://{item_id}", resource_paths)
 63 | 
 64 |     def test_register_catalog_tools(self):
 65 |         """Test that catalog tools are registered correctly."""
 66 |         # Call the method to register tools
 67 |         self.server._register_tools()
 68 | 
 69 |         # Check that the tool decorator was called
 70 |         self.server.mcp_server.tool.assert_called()
 71 | 
 72 |         # Get the tool functions
 73 |         tool_calls = self.server.mcp_server.tool.call_args_list
 74 |         
 75 |         # Instead of trying to extract names from the call args, just check that the decorator was called
 76 |         # the right number of times (at least 3 times for the catalog tools)
 77 |         self.assertGreaterEqual(len(tool_calls), 3)
 78 | 
 79 |     @patch("servicenow_mcp.tools.catalog_tools.list_catalog_items")
 80 |     def test_list_catalog_items_tool(self, mock_list_catalog_items):
 81 |         """Test the list_catalog_items tool."""
 82 |         # Mock the tool function
 83 |         mock_list_catalog_items.return_value = {
 84 |             "success": True,
 85 |             "message": "Retrieved 1 catalog items",
 86 |             "items": [
 87 |                 {
 88 |                     "sys_id": "item1",
 89 |                     "name": "Laptop",
 90 |                 }
 91 |             ],
 92 |         }
 93 | 
 94 |         # Register the tools
 95 |         self.server._register_tools()
 96 | 
 97 |         # Check that the tool decorator was called
 98 |         self.server.mcp_server.tool.assert_called()
 99 | 
100 |     @patch("servicenow_mcp.tools.catalog_tools.get_catalog_item")
101 |     def test_get_catalog_item_tool(self, mock_get_catalog_item):
102 |         """Test the get_catalog_item tool."""
103 |         # Mock the tool function
104 |         mock_get_catalog_item.return_value = {
105 |             "success": True,
106 |             "message": "Retrieved catalog item: Laptop",
107 |             "data": {
108 |                 "sys_id": "item1",
109 |                 "name": "Laptop",
110 |             },
111 |         }
112 | 
113 |         # Register the tools
114 |         self.server._register_tools()
115 | 
116 |         # Check that the tool decorator was called
117 |         self.server.mcp_server.tool.assert_called()
118 | 
119 |     @patch("servicenow_mcp.tools.catalog_tools.list_catalog_categories")
120 |     def test_list_catalog_categories_tool(self, mock_list_catalog_categories):
121 |         """Test the list_catalog_categories tool."""
122 |         # Mock the tool function
123 |         mock_list_catalog_categories.return_value = {
124 |             "success": True,
125 |             "message": "Retrieved 1 catalog categories",
126 |             "categories": [
127 |                 {
128 |                     "sys_id": "cat1",
129 |                     "title": "Hardware",
130 |                 }
131 |             ],
132 |         }
133 | 
134 |         # Register the tools
135 |         self.server._register_tools()
136 | 
137 |         # Check that the tool decorator was called
138 |         self.server.mcp_server.tool.assert_called()
139 | 
140 | 
141 | if __name__ == "__main__":
142 |     unittest.main() 
```

--------------------------------------------------------------------------------
/examples/debug_workflow_api.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python
  2 | """
  3 | Debug script for ServiceNow workflow API calls.
  4 | This script helps diagnose issues with the ServiceNow API by making direct calls
  5 | and printing detailed information about the requests and responses.
  6 | """
  7 | 
  8 | import os
  9 | import json
 10 | import logging
 11 | import requests
 12 | from dotenv import load_dotenv
 13 | 
 14 | # Set up logging
 15 | logging.basicConfig(level=logging.DEBUG)
 16 | logger = logging.getLogger(__name__)
 17 | 
 18 | # Load environment variables
 19 | load_dotenv()
 20 | 
 21 | # ServiceNow instance details
 22 | instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
 23 | username = os.getenv("SERVICENOW_USERNAME")
 24 | password = os.getenv("SERVICENOW_PASSWORD")
 25 | 
 26 | if not all([instance_url, username, password]):
 27 |     logger.error("Missing required environment variables. Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
 28 |     exit(1)
 29 | 
 30 | # Basic auth headers
 31 | auth = (username, password)
 32 | 
 33 | def debug_request(url, params=None, method="GET"):
 34 |     """Make a request to ServiceNow and print detailed debug information."""
 35 |     logger.info(f"Making {method} request to: {url}")
 36 |     logger.info(f"Parameters: {params}")
 37 |     
 38 |     try:
 39 |         if method == "GET":
 40 |             response = requests.get(url, auth=auth, params=params)
 41 |         elif method == "POST":
 42 |             response = requests.post(url, auth=auth, json=params)
 43 |         else:
 44 |             logger.error(f"Unsupported method: {method}")
 45 |             return
 46 |         
 47 |         logger.info(f"Status code: {response.status_code}")
 48 |         logger.info(f"Response headers: {response.headers}")
 49 |         
 50 |         # Try to parse as JSON
 51 |         try:
 52 |             json_response = response.json()
 53 |             logger.info(f"JSON response: {json.dumps(json_response, indent=2)}")
 54 |         except json.JSONDecodeError:
 55 |             logger.warning("Response is not valid JSON")
 56 |             logger.info(f"Raw response content: {response.content}")
 57 |         
 58 |         return response
 59 |     
 60 |     except requests.RequestException as e:
 61 |         logger.error(f"Request failed: {e}")
 62 |         return None
 63 | 
 64 | def test_list_workflows():
 65 |     """Test listing workflows."""
 66 |     logger.info("=== Testing list_workflows ===")
 67 |     url = f"{instance_url}/api/now/table/wf_workflow"
 68 |     params = {
 69 |         "sysparm_limit": 10,
 70 |     }
 71 |     return debug_request(url, params)
 72 | 
 73 | def test_list_workflows_active():
 74 |     """Test listing active workflows."""
 75 |     logger.info("=== Testing list_workflows with active=true ===")
 76 |     url = f"{instance_url}/api/now/table/wf_workflow"
 77 |     params = {
 78 |         "sysparm_limit": 10,
 79 |         "sysparm_query": "active=true",
 80 |     }
 81 |     return debug_request(url, params)
 82 | 
 83 | def test_get_workflow_details(workflow_id):
 84 |     """Test getting workflow details."""
 85 |     logger.info(f"=== Testing get_workflow_details for {workflow_id} ===")
 86 |     url = f"{instance_url}/api/now/table/wf_workflow/{workflow_id}"
 87 |     return debug_request(url)
 88 | 
 89 | def test_list_tables():
 90 |     """Test listing available tables to check API access."""
 91 |     logger.info("=== Testing list_tables ===")
 92 |     url = f"{instance_url}/api/now/table/sys_db_object"
 93 |     params = {
 94 |         "sysparm_limit": 5,
 95 |         "sysparm_fields": "name,label",
 96 |     }
 97 |     return debug_request(url, params)
 98 | 
 99 | def test_get_user_info():
100 |     """Test getting current user info to verify authentication."""
101 |     logger.info("=== Testing get_user_info ===")
102 |     url = f"{instance_url}/api/now/table/sys_user"
103 |     params = {
104 |         "sysparm_query": "user_name=" + username,
105 |         "sysparm_fields": "user_name,name,email,roles",
106 |     }
107 |     return debug_request(url, params)
108 | 
109 | if __name__ == "__main__":
110 |     logger.info(f"Testing ServiceNow API at {instance_url}")
111 |     
112 |     # First, verify authentication and basic API access
113 |     user_response = test_get_user_info()
114 |     if not user_response or user_response.status_code != 200:
115 |         logger.error("Authentication failed or user not found. Please check your credentials.")
116 |         exit(1)
117 |     
118 |     # Test listing tables to verify API access
119 |     tables_response = test_list_tables()
120 |     if not tables_response or tables_response.status_code != 200:
121 |         logger.error("Failed to list tables. API access may be restricted.")
122 |         exit(1)
123 |     
124 |     # Test workflow API calls
125 |     list_response = test_list_workflows()
126 |     active_response = test_list_workflows_active()
127 |     
128 |     # If we got any workflows, test getting details for the first one
129 |     if list_response and list_response.status_code == 200:
130 |         try:
131 |             workflows = list_response.json().get("result", [])
132 |             if workflows:
133 |                 workflow_id = workflows[0]["sys_id"]
134 |                 test_get_workflow_details(workflow_id)
135 |             else:
136 |                 logger.warning("No workflows found in the instance.")
137 |         except (json.JSONDecodeError, KeyError) as e:
138 |             logger.error(f"Error processing workflow list response: {e}")
139 |     
140 |     logger.info("Debug tests completed.") 
```

--------------------------------------------------------------------------------
/debug_workflow_api.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python
  2 | """
  3 | Debug script for ServiceNow workflow API calls.
  4 | This script helps diagnose issues with the ServiceNow API by making direct calls
  5 | and printing detailed information about the requests and responses.
  6 | """
  7 | 
  8 | import json
  9 | import logging
 10 | import os
 11 | 
 12 | import requests
 13 | from dotenv import load_dotenv
 14 | 
 15 | # Set up logging
 16 | logging.basicConfig(level=logging.DEBUG)
 17 | logger = logging.getLogger(__name__)
 18 | 
 19 | # Load environment variables
 20 | load_dotenv()
 21 | 
 22 | # ServiceNow instance details
 23 | instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
 24 | username = os.getenv("SERVICENOW_USERNAME")
 25 | password = os.getenv("SERVICENOW_PASSWORD")
 26 | 
 27 | if not all([instance_url, username, password]):
 28 |     logger.error("Missing required environment variables. Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
 29 |     exit(1)
 30 | 
 31 | # Basic auth headers
 32 | auth = (username, password)
 33 | 
 34 | def debug_request(url, params=None, method="GET"):
 35 |     """Make a request to ServiceNow and print detailed debug information."""
 36 |     logger.info(f"Making {method} request to: {url}")
 37 |     logger.info(f"Parameters: {params}")
 38 |     
 39 |     try:
 40 |         if method == "GET":
 41 |             response = requests.get(url, auth=auth, params=params)
 42 |         elif method == "POST":
 43 |             response = requests.post(url, auth=auth, json=params)
 44 |         else:
 45 |             logger.error(f"Unsupported method: {method}")
 46 |             return
 47 |         
 48 |         logger.info(f"Status code: {response.status_code}")
 49 |         logger.info(f"Response headers: {response.headers}")
 50 |         
 51 |         # Try to parse as JSON
 52 |         try:
 53 |             json_response = response.json()
 54 |             logger.info(f"JSON response: {json.dumps(json_response, indent=2)}")
 55 |         except json.JSONDecodeError:
 56 |             logger.warning("Response is not valid JSON")
 57 |             logger.info(f"Raw response content: {response.content}")
 58 |         
 59 |         return response
 60 |     
 61 |     except requests.RequestException as e:
 62 |         logger.error(f"Request failed: {e}")
 63 |         return None
 64 | 
 65 | def test_list_workflows():
 66 |     """Test listing workflows."""
 67 |     logger.info("=== Testing list_workflows ===")
 68 |     url = f"{instance_url}/api/now/table/wf_workflow"
 69 |     params = {
 70 |         "sysparm_limit": 10,
 71 |     }
 72 |     return debug_request(url, params)
 73 | 
 74 | def test_list_workflows_active():
 75 |     """Test listing active workflows."""
 76 |     logger.info("=== Testing list_workflows with active=true ===")
 77 |     url = f"{instance_url}/api/now/table/wf_workflow"
 78 |     params = {
 79 |         "sysparm_limit": 10,
 80 |         "sysparm_query": "active=true",
 81 |     }
 82 |     return debug_request(url, params)
 83 | 
 84 | def test_get_workflow_details(workflow_id):
 85 |     """Test getting workflow details."""
 86 |     logger.info(f"=== Testing get_workflow_details for {workflow_id} ===")
 87 |     url = f"{instance_url}/api/now/table/wf_workflow/{workflow_id}"
 88 |     return debug_request(url)
 89 | 
 90 | def test_list_tables():
 91 |     """Test listing available tables to check API access."""
 92 |     logger.info("=== Testing list_tables ===")
 93 |     url = f"{instance_url}/api/now/table/sys_db_object"
 94 |     params = {
 95 |         "sysparm_limit": 5,
 96 |         "sysparm_fields": "name,label",
 97 |     }
 98 |     return debug_request(url, params)
 99 | 
100 | def test_get_user_info():
101 |     """Test getting current user info to verify authentication."""
102 |     logger.info("=== Testing get_user_info ===")
103 |     url = f"{instance_url}/api/now/table/sys_user"
104 |     params = {
105 |         "sysparm_query": "user_name=" + username,
106 |         "sysparm_fields": "user_name,name,email,roles",
107 |     }
108 |     return debug_request(url, params)
109 | 
110 | if __name__ == "__main__":
111 |     logger.info(f"Testing ServiceNow API at {instance_url}")
112 |     
113 |     # First, verify authentication and basic API access
114 |     user_response = test_get_user_info()
115 |     if not user_response or user_response.status_code != 200:
116 |         logger.error("Authentication failed or user not found. Please check your credentials.")
117 |         exit(1)
118 |     
119 |     # Test listing tables to verify API access
120 |     tables_response = test_list_tables()
121 |     if not tables_response or tables_response.status_code != 200:
122 |         logger.error("Failed to list tables. API access may be restricted.")
123 |         exit(1)
124 |     
125 |     # Test workflow API calls
126 |     list_response = test_list_workflows()
127 |     active_response = test_list_workflows_active()
128 |     
129 |     # If we got any workflows, test getting details for the first one
130 |     if list_response and list_response.status_code == 200:
131 |         try:
132 |             workflows = list_response.json().get("result", [])
133 |             if workflows:
134 |                 workflow_id = workflows[0]["sys_id"]
135 |                 test_get_workflow_details(workflow_id)
136 |             else:
137 |                 logger.warning("No workflows found in the instance.")
138 |         except (json.JSONDecodeError, KeyError) as e:
139 |             logger.error(f"Error processing workflow list response: {e}")
140 |     
141 |     logger.info("Debug tests completed.") 
```

--------------------------------------------------------------------------------
/tests/test_workflow_tools_direct.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python
  2 | """
  3 | Test script for workflow_tools module.
  4 | This script directly tests the workflow_tools functions with proper authentication.
  5 | """
  6 | 
  7 | import os
  8 | import json
  9 | import logging
 10 | from dotenv import load_dotenv
 11 | 
 12 | from servicenow_mcp.auth.auth_manager import AuthManager
 13 | from servicenow_mcp.utils.config import ServerConfig, AuthConfig, AuthType, BasicAuthConfig
 14 | from servicenow_mcp.tools.workflow_tools import (
 15 |     list_workflows,
 16 |     get_workflow_details,
 17 |     list_workflow_versions,
 18 |     get_workflow_activities,
 19 | )
 20 | 
 21 | # Set up logging
 22 | logging.basicConfig(level=logging.DEBUG)
 23 | logger = logging.getLogger(__name__)
 24 | 
 25 | # Load environment variables
 26 | load_dotenv()
 27 | 
 28 | def setup_auth_and_config():
 29 |     """Set up authentication and server configuration."""
 30 |     instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
 31 |     username = os.getenv("SERVICENOW_USERNAME")
 32 |     password = os.getenv("SERVICENOW_PASSWORD")
 33 |     
 34 |     if not all([instance_url, username, password]):
 35 |         logger.error("Missing required environment variables. Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
 36 |         exit(1)
 37 |     
 38 |     # Create authentication configuration
 39 |     auth_config = AuthConfig(
 40 |         type=AuthType.BASIC,
 41 |         basic=BasicAuthConfig(username=username, password=password),
 42 |     )
 43 |     
 44 |     # Create server configuration
 45 |     server_config = ServerConfig(
 46 |         instance_url=instance_url,
 47 |         auth=auth_config,
 48 |     )
 49 |     
 50 |     # Create authentication manager
 51 |     auth_manager = AuthManager(auth_config)
 52 |     
 53 |     return auth_manager, server_config
 54 | 
 55 | def print_result(name, result):
 56 |     """Print the result of a function call."""
 57 |     logger.info(f"=== Result of {name} ===")
 58 |     if "error" in result:
 59 |         logger.error(f"Error: {result['error']}")
 60 |     else:
 61 |         logger.info(json.dumps(result, indent=2))
 62 | 
 63 | def test_list_workflows(auth_manager, server_config):
 64 |     """Test the list_workflows function."""
 65 |     logger.info("Testing list_workflows...")
 66 |     
 67 |     # Test with default parameters
 68 |     result = list_workflows(auth_manager, server_config, {})
 69 |     print_result("list_workflows (default)", result)
 70 |     
 71 |     # Test with active=True
 72 |     result = list_workflows(auth_manager, server_config, {"active": True})
 73 |     print_result("list_workflows (active=True)", result)
 74 |     
 75 |     return result
 76 | 
 77 | def test_get_workflow_details(auth_manager, server_config, workflow_id):
 78 |     """Test the get_workflow_details function."""
 79 |     logger.info(f"Testing get_workflow_details for workflow {workflow_id}...")
 80 |     
 81 |     result = get_workflow_details(auth_manager, server_config, {"workflow_id": workflow_id})
 82 |     print_result("get_workflow_details", result)
 83 |     
 84 |     return result
 85 | 
 86 | def test_list_workflow_versions(auth_manager, server_config, workflow_id):
 87 |     """Test the list_workflow_versions function."""
 88 |     logger.info(f"Testing list_workflow_versions for workflow {workflow_id}...")
 89 |     
 90 |     result = list_workflow_versions(auth_manager, server_config, {"workflow_id": workflow_id})
 91 |     print_result("list_workflow_versions", result)
 92 |     
 93 |     return result
 94 | 
 95 | def test_get_workflow_activities(auth_manager, server_config, workflow_id):
 96 |     """Test the get_workflow_activities function."""
 97 |     logger.info(f"Testing get_workflow_activities for workflow {workflow_id}...")
 98 |     
 99 |     result = get_workflow_activities(auth_manager, server_config, {"workflow_id": workflow_id})
100 |     print_result("get_workflow_activities", result)
101 |     
102 |     return result
103 | 
104 | def test_with_swapped_params(auth_manager, server_config):
105 |     """Test functions with swapped parameters to verify our fix works."""
106 |     logger.info("Testing with swapped parameters...")
107 |     
108 |     # Test list_workflows with swapped parameters
109 |     result = list_workflows(server_config, auth_manager, {})
110 |     print_result("list_workflows (swapped params)", result)
111 |     
112 |     return result
113 | 
114 | if __name__ == "__main__":
115 |     logger.info("Testing workflow_tools module...")
116 |     
117 |     # Set up authentication and server configuration
118 |     auth_manager, server_config = setup_auth_and_config()
119 |     
120 |     # Test list_workflows
121 |     workflows_result = test_list_workflows(auth_manager, server_config)
122 |     
123 |     # If we got any workflows, test the other functions
124 |     if "workflows" in workflows_result and workflows_result["workflows"]:
125 |         workflow_id = workflows_result["workflows"][0]["sys_id"]
126 |         
127 |         # Test get_workflow_details
128 |         test_get_workflow_details(auth_manager, server_config, workflow_id)
129 |         
130 |         # Test list_workflow_versions
131 |         test_list_workflow_versions(auth_manager, server_config, workflow_id)
132 |         
133 |         # Test get_workflow_activities
134 |         test_get_workflow_activities(auth_manager, server_config, workflow_id)
135 |     else:
136 |         logger.warning("No workflows found, skipping detail tests.")
137 |     
138 |     # Test with swapped parameters
139 |     test_with_swapped_params(auth_manager, server_config)
140 |     
141 |     logger.info("Tests completed.") 
```

--------------------------------------------------------------------------------
/examples/changeset_management_demo.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Changeset Management Demo
  4 | 
  5 | This script demonstrates how to use the ServiceNow MCP server to manage changesets.
  6 | It shows how to create, update, commit, and publish changesets, as well as how to
  7 | add files to changesets and retrieve changeset details.
  8 | """
  9 | 
 10 | import json
 11 | import os
 12 | import sys
 13 | from dotenv import load_dotenv
 14 | 
 15 | # Add the parent directory to the path so we can import the servicenow_mcp package
 16 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 17 | 
 18 | from servicenow_mcp.auth.auth_manager import AuthManager
 19 | from servicenow_mcp.tools.changeset_tools import (
 20 |     add_file_to_changeset,
 21 |     commit_changeset,
 22 |     create_changeset,
 23 |     get_changeset_details,
 24 |     list_changesets,
 25 |     publish_changeset,
 26 |     update_changeset,
 27 | )
 28 | from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
 29 | 
 30 | # Load environment variables from .env file
 31 | load_dotenv()
 32 | 
 33 | # Get ServiceNow credentials from environment variables
 34 | instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
 35 | username = os.getenv("SERVICENOW_USERNAME")
 36 | password = os.getenv("SERVICENOW_PASSWORD")
 37 | 
 38 | if not all([instance_url, username, password]):
 39 |     print("Error: Missing required environment variables.")
 40 |     print("Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
 41 |     sys.exit(1)
 42 | 
 43 | # Create server configuration
 44 | auth_config = AuthConfig(
 45 |     auth_type=AuthType.BASIC,
 46 |     basic=BasicAuthConfig(
 47 |         username=username,
 48 |         password=password,
 49 |     ),
 50 | )
 51 | 
 52 | server_config = ServerConfig(
 53 |     instance_url=instance_url,
 54 |     auth=auth_config,
 55 | )
 56 | 
 57 | # Create authentication manager
 58 | auth_manager = AuthManager(auth_config)
 59 | auth_manager.instance_url = instance_url
 60 | 
 61 | 
 62 | def print_json(data):
 63 |     """Print JSON data in a readable format."""
 64 |     print(json.dumps(data, indent=2))
 65 | 
 66 | 
 67 | def main():
 68 |     """Run the changeset management demo."""
 69 |     print("\n=== Changeset Management Demo ===\n")
 70 | 
 71 |     # Step 1: List existing changesets
 72 |     print("Step 1: Listing existing changesets...")
 73 |     result = list_changesets(auth_manager, server_config, {
 74 |         "limit": 5,
 75 |         "timeframe": "recent",
 76 |     })
 77 |     print_json(result)
 78 |     print("\n")
 79 | 
 80 |     # Step 2: Create a new changeset
 81 |     print("Step 2: Creating a new changeset...")
 82 |     create_result = create_changeset(auth_manager, server_config, {
 83 |         "name": "Demo Changeset",
 84 |         "description": "A demonstration changeset created by the MCP demo script",
 85 |         "application": "Global",  # Use a valid application name for your instance
 86 |         "developer": username,
 87 |     })
 88 |     print_json(create_result)
 89 |     print("\n")
 90 | 
 91 |     if not create_result.get("success"):
 92 |         print("Failed to create changeset. Exiting.")
 93 |         sys.exit(1)
 94 | 
 95 |     # Get the changeset ID from the create result
 96 |     changeset_id = create_result["changeset"]["sys_id"]
 97 |     print(f"Created changeset with ID: {changeset_id}")
 98 |     print("\n")
 99 | 
100 |     # Step 3: Update the changeset
101 |     print("Step 3: Updating the changeset...")
102 |     update_result = update_changeset(auth_manager, server_config, {
103 |         "changeset_id": changeset_id,
104 |         "name": "Demo Changeset - Updated",
105 |         "description": "An updated demonstration changeset",
106 |     })
107 |     print_json(update_result)
108 |     print("\n")
109 | 
110 |     # Step 4: Add a file to the changeset
111 |     print("Step 4: Adding a file to the changeset...")
112 |     file_content = """
113 | function demoFunction() {
114 |     // This is a demonstration function
115 |     gs.info('Hello from the demo changeset!');
116 |     return 'Demo function executed successfully';
117 | }
118 | """
119 |     add_file_result = add_file_to_changeset(auth_manager, server_config, {
120 |         "changeset_id": changeset_id,
121 |         "file_path": "scripts/demo_function.js",
122 |         "file_content": file_content,
123 |     })
124 |     print_json(add_file_result)
125 |     print("\n")
126 | 
127 |     # Step 5: Get changeset details
128 |     print("Step 5: Getting changeset details...")
129 |     details_result = get_changeset_details(auth_manager, server_config, {
130 |         "changeset_id": changeset_id,
131 |     })
132 |     print_json(details_result)
133 |     print("\n")
134 | 
135 |     # Step 6: Commit the changeset
136 |     print("Step 6: Committing the changeset...")
137 |     commit_result = commit_changeset(auth_manager, server_config, {
138 |         "changeset_id": changeset_id,
139 |         "commit_message": "Completed the demo changeset",
140 |     })
141 |     print_json(commit_result)
142 |     print("\n")
143 | 
144 |     # Step 7: Publish the changeset
145 |     print("Step 7: Publishing the changeset...")
146 |     publish_result = publish_changeset(auth_manager, server_config, {
147 |         "changeset_id": changeset_id,
148 |         "publish_notes": "Demo changeset ready for deployment",
149 |     })
150 |     print_json(publish_result)
151 |     print("\n")
152 | 
153 |     # Step 8: Get final changeset details
154 |     print("Step 8: Getting final changeset details...")
155 |     final_details_result = get_changeset_details(auth_manager, server_config, {
156 |         "changeset_id": changeset_id,
157 |     })
158 |     print_json(final_details_result)
159 |     print("\n")
160 | 
161 |     print("=== Changeset Management Demo Completed ===")
162 | 
163 | 
164 | if __name__ == "__main__":
165 |     main() 
```

--------------------------------------------------------------------------------
/src/servicenow_mcp/auth/auth_manager.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Authentication manager for the ServiceNow MCP server.
  3 | """
  4 | 
  5 | import base64
  6 | import logging
  7 | import os
  8 | from typing import Dict, Optional
  9 | 
 10 | import requests
 11 | from requests.auth import HTTPBasicAuth
 12 | 
 13 | from servicenow_mcp.utils.config import AuthConfig, AuthType
 14 | 
 15 | 
 16 | logger = logging.getLogger(__name__)
 17 | 
 18 | 
 19 | class AuthManager:
 20 |     """
 21 |     Authentication manager for ServiceNow API.
 22 |     
 23 |     This class handles authentication with the ServiceNow API using
 24 |     different authentication methods.
 25 |     """
 26 |     
 27 |     def __init__(self, config: AuthConfig, instance_url: str = None):
 28 |         """
 29 |         Initialize the authentication manager.
 30 |         
 31 |         Args:
 32 |             config: Authentication configuration.
 33 |             instance_url: ServiceNow instance URL.
 34 |         """
 35 |         self.config = config
 36 |         self.instance_url = instance_url
 37 |         self.token: Optional[str] = None
 38 |         self.token_type: Optional[str] = None
 39 |     
 40 |     def get_headers(self) -> Dict[str, str]:
 41 |         """
 42 |         Get the authentication headers for API requests.
 43 |         
 44 |         Returns:
 45 |             Dict[str, str]: Headers to include in API requests.
 46 |         """
 47 |         headers = {
 48 |             "Accept": "application/json",
 49 |             "Content-Type": "application/json",
 50 |         }
 51 |         
 52 |         if self.config.type == AuthType.BASIC:
 53 |             if not self.config.basic:
 54 |                 raise ValueError("Basic auth configuration is required")
 55 |             
 56 |             auth_str = f"{self.config.basic.username}:{self.config.basic.password}"
 57 |             encoded = base64.b64encode(auth_str.encode()).decode()
 58 |             headers["Authorization"] = f"Basic {encoded}"
 59 |         
 60 |         elif self.config.type == AuthType.OAUTH:
 61 |             if not self.token:
 62 |                 self._get_oauth_token()
 63 |             
 64 |             headers["Authorization"] = f"{self.token_type} {self.token}"
 65 |         
 66 |         elif self.config.type == AuthType.API_KEY:
 67 |             if not self.config.api_key:
 68 |                 raise ValueError("API key configuration is required")
 69 |             
 70 |             headers[self.config.api_key.header_name] = self.config.api_key.api_key
 71 |         
 72 |         return headers
 73 |     
 74 |     def _get_oauth_token(self):
 75 |         """
 76 |         Get an OAuth token from ServiceNow.
 77 |         
 78 |         Raises:
 79 |             ValueError: If OAuth configuration is missing or token request fails.
 80 |         """
 81 |         if not self.config.oauth:
 82 |             raise ValueError("OAuth configuration is required")
 83 |         oauth_config = self.config.oauth
 84 | 
 85 |         # Determine token URL
 86 |         token_url = oauth_config.token_url
 87 |         if not token_url:
 88 |             if not self.instance_url:
 89 |                 raise ValueError("Instance URL is required for OAuth authentication")
 90 |             instance_parts = self.instance_url.split(".")
 91 |             if len(instance_parts) < 2:
 92 |                 raise ValueError(f"Invalid instance URL: {self.instance_url}")
 93 |             instance_name = instance_parts[0].split("//")[-1]
 94 |             token_url = f"https://{instance_name}.service-now.com/oauth_token.do"
 95 | 
 96 |         # Prepare Authorization header
 97 |         auth_str = f"{oauth_config.client_id}:{oauth_config.client_secret}"
 98 |         auth_header = base64.b64encode(auth_str.encode()).decode()
 99 |         headers = {
100 |             "Authorization": f"Basic {auth_header}",
101 |             "Content-Type": "application/x-www-form-urlencoded"
102 |         }
103 | 
104 |         # Try client_credentials grant first
105 |         data_client_credentials = {
106 |             "grant_type": "client_credentials"
107 |         }
108 |         
109 |         logger.info("Attempting client_credentials grant...")
110 |         response = requests.post(token_url, headers=headers, data=data_client_credentials)
111 |         
112 |         logger.info(f"client_credentials response status: {response.status_code}")
113 |         logger.info(f"client_credentials response body: {response.text}")
114 |         
115 |         if response.status_code == 200:
116 |             token_data = response.json()
117 |             self.token = token_data.get("access_token")
118 |             self.token_type = token_data.get("token_type", "Bearer")
119 |             return
120 | 
121 |         # Try password grant if client_credentials failed
122 |         if oauth_config.username and oauth_config.password:
123 |             data_password = {
124 |                 "grant_type": "password",
125 |                 "username": oauth_config.username,
126 |                 "password": oauth_config.password
127 |             }
128 |             
129 |             logger.info("Attempting password grant...")
130 |             response = requests.post(token_url, headers=headers, data=data_password)
131 |             
132 |             logger.info(f"password grant response status: {response.status_code}")
133 |             logger.info(f"password grant response body: {response.text}")
134 |             
135 |             if response.status_code == 200:
136 |                 token_data = response.json()
137 |                 self.token = token_data.get("access_token")
138 |                 self.token_type = token_data.get("token_type", "Bearer")
139 |                 return
140 | 
141 |         raise ValueError("Failed to get OAuth token using both client_credentials and password grants.")
142 |     
143 |     def refresh_token(self):
144 |         """Refresh the OAuth token if using OAuth authentication."""
145 |         if self.config.type == AuthType.OAUTH:
146 |             self._get_oauth_token() 
```

--------------------------------------------------------------------------------
/src/servicenow_mcp/tools/__init__.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tools module for the ServiceNow MCP server.
  3 | """
  4 | 
  5 | # Import tools as they are implemented
  6 | from servicenow_mcp.tools.catalog_optimization import (
  7 |     get_optimization_recommendations,
  8 |     update_catalog_item,
  9 | )
 10 | from servicenow_mcp.tools.catalog_tools import (
 11 |     create_catalog_category,
 12 |     get_catalog_item,
 13 |     list_catalog_categories,
 14 |     list_catalog_items,
 15 |     move_catalog_items,
 16 |     update_catalog_category,
 17 | )
 18 | from servicenow_mcp.tools.catalog_variables import (
 19 |     create_catalog_item_variable,
 20 |     list_catalog_item_variables,
 21 |     update_catalog_item_variable,
 22 | )
 23 | from servicenow_mcp.tools.change_tools import (
 24 |     add_change_task,
 25 |     approve_change,
 26 |     create_change_request,
 27 |     get_change_request_details,
 28 |     list_change_requests,
 29 |     reject_change,
 30 |     submit_change_for_approval,
 31 |     update_change_request,
 32 | )
 33 | from servicenow_mcp.tools.changeset_tools import (
 34 |     add_file_to_changeset,
 35 |     commit_changeset,
 36 |     create_changeset,
 37 |     get_changeset_details,
 38 |     list_changesets,
 39 |     publish_changeset,
 40 |     update_changeset,
 41 | )
 42 | from servicenow_mcp.tools.incident_tools import (
 43 |     add_comment,
 44 |     create_incident,
 45 |     list_incidents,
 46 |     resolve_incident,
 47 |     update_incident,
 48 |     get_incident_by_number,
 49 | )
 50 | from servicenow_mcp.tools.knowledge_base import (
 51 |     create_article,
 52 |     create_category,
 53 |     create_knowledge_base,
 54 |     get_article,
 55 |     list_articles,
 56 |     list_knowledge_bases,
 57 |     publish_article,
 58 |     update_article,
 59 |     list_categories,
 60 | )
 61 | from servicenow_mcp.tools.script_include_tools import (
 62 |     create_script_include,
 63 |     delete_script_include,
 64 |     get_script_include,
 65 |     list_script_includes,
 66 |     update_script_include,
 67 | )
 68 | from servicenow_mcp.tools.user_tools import (
 69 |     create_user,
 70 |     update_user,
 71 |     get_user,
 72 |     list_users,
 73 |     create_group,
 74 |     update_group,
 75 |     add_group_members,
 76 |     remove_group_members,
 77 |     list_groups,
 78 | )
 79 | from servicenow_mcp.tools.workflow_tools import (
 80 |     activate_workflow,
 81 |     add_workflow_activity,
 82 |     create_workflow,
 83 |     deactivate_workflow,
 84 |     delete_workflow_activity,
 85 |     get_workflow_activities,
 86 |     get_workflow_details,
 87 |     list_workflow_versions,
 88 |     list_workflows,
 89 |     reorder_workflow_activities,
 90 |     update_workflow,
 91 |     update_workflow_activity,
 92 | )
 93 | from servicenow_mcp.tools.story_tools import (
 94 |     create_story,
 95 |     update_story,
 96 |     list_stories,
 97 |     list_story_dependencies,
 98 |     create_story_dependency,
 99 |     delete_story_dependency,
100 | )
101 | from servicenow_mcp.tools.epic_tools import (
102 |     create_epic,
103 |     update_epic,
104 |     list_epics,
105 | )
106 | from servicenow_mcp.tools.scrum_task_tools import (
107 |     create_scrum_task,
108 |     update_scrum_task,
109 |     list_scrum_tasks,
110 | )
111 | from servicenow_mcp.tools.project_tools import (
112 |     create_project,
113 |     update_project,
114 |     list_projects,
115 | )
116 | # from servicenow_mcp.tools.problem_tools import create_problem, update_problem
117 | # from servicenow_mcp.tools.request_tools import create_request, update_request
118 | 
119 | __all__ = [
120 |     # Incident tools
121 |     "create_incident",
122 |     "update_incident",
123 |     "add_comment",
124 |     "resolve_incident",
125 |     "list_incidents",
126 |     "get_incident_by_number",
127 |     
128 |     # Catalog tools
129 |     "list_catalog_items",
130 |     "get_catalog_item",
131 |     "list_catalog_categories",
132 |     "create_catalog_category",
133 |     "update_catalog_category",
134 |     "move_catalog_items",
135 |     "get_optimization_recommendations",
136 |     "update_catalog_item",
137 |     "create_catalog_item_variable",
138 |     "list_catalog_item_variables",
139 |     "update_catalog_item_variable",
140 |     
141 |     # Change management tools
142 |     "create_change_request",
143 |     "update_change_request",
144 |     "list_change_requests",
145 |     "get_change_request_details",
146 |     "add_change_task",
147 |     "submit_change_for_approval",
148 |     "approve_change",
149 |     "reject_change",
150 |     
151 |     # Workflow management tools
152 |     "list_workflows",
153 |     "get_workflow_details",
154 |     "list_workflow_versions",
155 |     "get_workflow_activities",
156 |     "create_workflow",
157 |     "update_workflow",
158 |     "activate_workflow",
159 |     "deactivate_workflow",
160 |     "add_workflow_activity",
161 |     "update_workflow_activity",
162 |     "delete_workflow_activity",
163 |     "reorder_workflow_activities",
164 |     
165 |     # Changeset tools
166 |     "list_changesets",
167 |     "get_changeset_details",
168 |     "create_changeset",
169 |     "update_changeset",
170 |     "commit_changeset",
171 |     "publish_changeset",
172 |     "add_file_to_changeset",
173 |     
174 |     # Script Include tools
175 |     "list_script_includes",
176 |     "get_script_include",
177 |     "create_script_include",
178 |     "update_script_include",
179 |     "delete_script_include",
180 |     
181 |     # Knowledge Base tools
182 |     "create_knowledge_base",
183 |     "list_knowledge_bases",
184 |     "create_category",
185 |     "list_categories",
186 |     "create_article",
187 |     "update_article",
188 |     "publish_article",
189 |     "list_articles",
190 |     "get_article",
191 |     
192 |     # User management tools
193 |     "create_user",
194 |     "update_user",
195 |     "get_user",
196 |     "list_users",
197 |     "create_group",
198 |     "update_group",
199 |     "add_group_members",
200 |     "remove_group_members",
201 |     "list_groups",
202 | 
203 |     # Story tools
204 |     "create_story",
205 |     "update_story",
206 |     "list_stories",
207 |     "list_story_dependencies",
208 |     "create_story_dependency",
209 |     "delete_story_dependency",
210 |     
211 |     # Epic tools
212 |     "create_epic",
213 |     "update_epic",
214 |     "list_epics",
215 | 
216 |     # Scrum Task tools
217 |     "create_scrum_task",
218 |     "update_scrum_task",
219 |     "list_scrum_tasks",
220 | 
221 |     # Project tools
222 |     "create_project",
223 |     "update_project",
224 |     "list_projects",
225 | 
226 |     
227 |     # Future tools
228 |     # "create_problem",
229 |     # "update_problem",
230 |     # "create_request",
231 |     # "update_request",
232 | ] 
```

--------------------------------------------------------------------------------
/docs/incident_management.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Incident Management
  2 | 
  3 | This document describes the incident management functionality provided by the ServiceNow MCP server.
  4 | 
  5 | ## Overview
  6 | 
  7 | The incident management module allows LLMs to interact with ServiceNow incidents through the Model Context Protocol (MCP). It provides resources for querying incident data and tools for creating, updating, and resolving incidents.
  8 | 
  9 | ## Resources
 10 | 
 11 | ### List Incidents
 12 | 
 13 | Retrieves a list of incidents from ServiceNow.
 14 | 
 15 | **Resource Name:** `incidents`
 16 | 
 17 | **Parameters:**
 18 | - `limit` (int, default: 10): Maximum number of incidents to return
 19 | - `offset` (int, default: 0): Offset for pagination
 20 | - `state` (string, optional): Filter by incident state
 21 | - `assigned_to` (string, optional): Filter by assigned user
 22 | - `category` (string, optional): Filter by category
 23 | - `query` (string, optional): Search query for incidents
 24 | 
 25 | **Example:**
 26 | ```python
 27 | incidents = await mcp.get_resource("servicenow", "incidents", {
 28 |     "limit": 5,
 29 |     "state": "1",  # New
 30 |     "category": "Software"
 31 | })
 32 | 
 33 | for incident in incidents:
 34 |     print(f"{incident.number}: {incident.short_description}")
 35 | ```
 36 | 
 37 | ### Get Incident
 38 | 
 39 | Retrieves a specific incident from ServiceNow by ID or number.
 40 | 
 41 | **Resource Name:** `incident`
 42 | 
 43 | **Parameters:**
 44 | - `incident_id` (string): Incident ID or sys_id
 45 | 
 46 | **Example:**
 47 | ```python
 48 | incident = await mcp.get_resource("servicenow", "incident", "INC0010001")
 49 | print(f"Incident: {incident.number}")
 50 | print(f"Description: {incident.short_description}")
 51 | print(f"State: {incident.state}")
 52 | ```
 53 | 
 54 | ## Tools
 55 | 
 56 | ### Create Incident
 57 | 
 58 | Creates a new incident in ServiceNow.
 59 | 
 60 | **Tool Name:** `create_incident`
 61 | 
 62 | **Parameters:**
 63 | - `short_description` (string, required): Short description of the incident
 64 | - `description` (string, optional): Detailed description of the incident
 65 | - `caller_id` (string, optional): User who reported the incident
 66 | - `category` (string, optional): Category of the incident
 67 | - `subcategory` (string, optional): Subcategory of the incident
 68 | - `priority` (string, optional): Priority of the incident
 69 | - `impact` (string, optional): Impact of the incident
 70 | - `urgency` (string, optional): Urgency of the incident
 71 | - `assigned_to` (string, optional): User assigned to the incident
 72 | - `assignment_group` (string, optional): Group assigned to the incident
 73 | 
 74 | **Example:**
 75 | ```python
 76 | result = await mcp.use_tool("servicenow", "create_incident", {
 77 |     "short_description": "Email service is down",
 78 |     "description": "Users are unable to send or receive emails.",
 79 |     "category": "Software",
 80 |     "priority": "1"
 81 | })
 82 | 
 83 | print(f"Incident created: {result.incident_number}")
 84 | ```
 85 | 
 86 | ### Update Incident
 87 | 
 88 | Updates an existing incident in ServiceNow.
 89 | 
 90 | **Tool Name:** `update_incident`
 91 | 
 92 | **Parameters:**
 93 | - `incident_id` (string, required): Incident ID or sys_id
 94 | - `short_description` (string, optional): Short description of the incident
 95 | - `description` (string, optional): Detailed description of the incident
 96 | - `state` (string, optional): State of the incident
 97 | - `category` (string, optional): Category of the incident
 98 | - `subcategory` (string, optional): Subcategory of the incident
 99 | - `priority` (string, optional): Priority of the incident
100 | - `impact` (string, optional): Impact of the incident
101 | - `urgency` (string, optional): Urgency of the incident
102 | - `assigned_to` (string, optional): User assigned to the incident
103 | - `assignment_group` (string, optional): Group assigned to the incident
104 | - `work_notes` (string, optional): Work notes to add to the incident
105 | - `close_notes` (string, optional): Close notes to add to the incident
106 | - `close_code` (string, optional): Close code for the incident
107 | 
108 | **Example:**
109 | ```python
110 | result = await mcp.use_tool("servicenow", "update_incident", {
111 |     "incident_id": "INC0010001",
112 |     "priority": "2",
113 |     "assigned_to": "admin",
114 |     "work_notes": "Investigating the issue."
115 | })
116 | 
117 | print(f"Incident updated: {result.success}")
118 | ```
119 | 
120 | ### Add Comment
121 | 
122 | Adds a comment to an incident in ServiceNow.
123 | 
124 | **Tool Name:** `add_comment`
125 | 
126 | **Parameters:**
127 | - `incident_id` (string, required): Incident ID or sys_id
128 | - `comment` (string, required): Comment to add to the incident
129 | - `is_work_note` (boolean, default: false): Whether the comment is a work note
130 | 
131 | **Example:**
132 | ```python
133 | result = await mcp.use_tool("servicenow", "add_comment", {
134 |     "incident_id": "INC0010001",
135 |     "comment": "The issue is being investigated by the network team.",
136 |     "is_work_note": true
137 | })
138 | 
139 | print(f"Comment added: {result.success}")
140 | ```
141 | 
142 | ### Resolve Incident
143 | 
144 | Resolves an incident in ServiceNow.
145 | 
146 | **Tool Name:** `resolve_incident`
147 | 
148 | **Parameters:**
149 | - `incident_id` (string, required): Incident ID or sys_id
150 | - `resolution_code` (string, required): Resolution code for the incident
151 | - `resolution_notes` (string, required): Resolution notes for the incident
152 | 
153 | **Example:**
154 | ```python
155 | result = await mcp.use_tool("servicenow", "resolve_incident", {
156 |     "incident_id": "INC0010001",
157 |     "resolution_code": "Solved (Permanently)",
158 |     "resolution_notes": "The email service has been restored."
159 | })
160 | 
161 | print(f"Incident resolved: {result.success}")
162 | ```
163 | 
164 | ## State Values
165 | 
166 | ServiceNow incident states are represented by numeric values:
167 | 
168 | - `1`: New
169 | - `2`: In Progress
170 | - `3`: On Hold
171 | - `4`: Resolved
172 | - `5`: Closed
173 | - `6`: Canceled
174 | 
175 | ## Priority Values
176 | 
177 | ServiceNow incident priorities are represented by numeric values:
178 | 
179 | - `1`: Critical
180 | - `2`: High
181 | - `3`: Moderate
182 | - `4`: Low
183 | - `5`: Planning
184 | 
185 | ## Testing
186 | 
187 | You can test the incident management functionality using the provided test script:
188 | 
189 | ```bash
190 | python examples/test_incidents.py
191 | ```
192 | 
193 | Make sure to set the required environment variables in your `.env` file:
194 | 
195 | ```
196 | SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com
197 | SERVICENOW_USERNAME=your-username
198 | SERVICENOW_PASSWORD=your-password
199 | SERVICENOW_AUTH_TYPE=basic
200 | ``` 
```

--------------------------------------------------------------------------------
/examples/catalog_integration_test.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python
  2 | """
  3 | ServiceNow Catalog Integration Test
  4 | 
  5 | This script demonstrates how to use the ServiceNow MCP server to interact with
  6 | the ServiceNow Service Catalog. It serves as an integration test to verify that
  7 | the catalog functionality works correctly with a real ServiceNow instance.
  8 | 
  9 | Prerequisites:
 10 | 1. Valid ServiceNow credentials with access to the Service Catalog
 11 | 2. ServiceNow MCP package installed
 12 | 
 13 | Usage:
 14 |     python examples/catalog_integration_test.py
 15 | """
 16 | 
 17 | import os
 18 | import sys
 19 | 
 20 | from dotenv import load_dotenv
 21 | 
 22 | # Add the project root to the Python path
 23 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
 24 | 
 25 | from servicenow_mcp.auth.auth_manager import AuthManager
 26 | from servicenow_mcp.tools.catalog_tools import (
 27 |     GetCatalogItemParams,
 28 |     ListCatalogCategoriesParams,
 29 |     ListCatalogItemsParams,
 30 |     get_catalog_item,
 31 |     list_catalog_categories,
 32 |     list_catalog_items,
 33 | )
 34 | from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
 35 | 
 36 | 
 37 | def main():
 38 |     """Run the catalog integration test."""
 39 |     # Load environment variables
 40 |     load_dotenv()
 41 | 
 42 |     # Get configuration from environment variables
 43 |     instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
 44 |     username = os.getenv("SERVICENOW_USERNAME")
 45 |     password = os.getenv("SERVICENOW_PASSWORD")
 46 | 
 47 |     if not instance_url or not username or not password:
 48 |         print("Error: Missing required environment variables.")
 49 |         print("Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
 50 |         sys.exit(1)
 51 | 
 52 |     print(f"Connecting to ServiceNow instance: {instance_url}")
 53 | 
 54 |     # Create the server config
 55 |     config = ServerConfig(
 56 |         instance_url=instance_url,
 57 |         auth=AuthConfig(
 58 |             type=AuthType.BASIC,
 59 |             basic=BasicAuthConfig(username=username, password=password),
 60 |         ),
 61 |     )
 62 |     
 63 |     # Create the auth manager
 64 |     auth_manager = AuthManager(config.auth)
 65 | 
 66 |     # Test listing catalog categories
 67 |     print("\n=== Testing List Catalog Categories ===")
 68 |     category_id = test_list_catalog_categories(config, auth_manager)
 69 | 
 70 |     # Test listing catalog items
 71 |     print("\n=== Testing List Catalog Items ===")
 72 |     item_id = test_list_catalog_items(config, auth_manager, category_id)
 73 | 
 74 |     # Test getting a specific catalog item
 75 |     if item_id:
 76 |         print("\n=== Testing Get Catalog Item ===")
 77 |         test_get_catalog_item(config, auth_manager, item_id)
 78 | 
 79 | 
 80 | def test_list_catalog_categories(config, auth_manager):
 81 |     """Test listing catalog categories."""
 82 |     print("Fetching catalog categories...")
 83 |     
 84 |     # Create the parameters
 85 |     params = ListCatalogCategoriesParams(
 86 |         limit=5,
 87 |         offset=0,
 88 |         query="",
 89 |         active=True,
 90 |     )
 91 | 
 92 |     # Call the tool function directly
 93 |     result = list_catalog_categories(config, auth_manager, params)
 94 |     
 95 |     # Print the result
 96 |     print(f"Found {result.get('total', 0)} catalog categories:")
 97 |     for i, category in enumerate(result.get("categories", []), 1):
 98 |         print(f"{i}. {category.get('title')} (ID: {category.get('sys_id')})")
 99 |         print(f"   Description: {category.get('description', 'N/A')}")
100 |         print()
101 |     
102 |     # Save the first category ID for later use
103 |     if result.get("categories"):
104 |         return result["categories"][0]["sys_id"]
105 |     return None
106 | 
107 | 
108 | def test_list_catalog_items(config, auth_manager, category_id=None):
109 |     """Test listing catalog items."""
110 |     print("Fetching catalog items...")
111 |     
112 |     # Create the parameters
113 |     params = ListCatalogItemsParams(
114 |         limit=5,
115 |         offset=0,
116 |         query="",
117 |         category=category_id,  # Filter by category if provided
118 |         active=True,
119 |     )
120 | 
121 |     # Call the tool function directly
122 |     result = list_catalog_items(config, auth_manager, params)
123 |     
124 |     # Print the result
125 |     print(f"Found {result.get('total', 0)} catalog items:")
126 |     for i, item in enumerate(result.get("items", []), 1):
127 |         print(f"{i}. {item.get('name')} (ID: {item.get('sys_id')})")
128 |         print(f"   Description: {item.get('short_description', 'N/A')}")
129 |         print(f"   Category: {item.get('category', 'N/A')}")
130 |         print(f"   Price: {item.get('price', 'N/A')}")
131 |         print()
132 |     
133 |     # Save the first item ID for later use
134 |     if result.get("items"):
135 |         return result["items"][0]["sys_id"]
136 |     return None
137 | 
138 | 
139 | def test_get_catalog_item(config, auth_manager, item_id):
140 |     """Test getting a specific catalog item."""
141 |     print(f"Fetching details for catalog item: {item_id}")
142 |     
143 |     # Create the parameters
144 |     params = GetCatalogItemParams(
145 |         item_id=item_id,
146 |     )
147 | 
148 |     # Call the tool function directly
149 |     result = get_catalog_item(config, auth_manager, params)
150 |     
151 |     # Print the result
152 |     if result.success:
153 |         print(f"Retrieved catalog item: {result.data.get('name')} (ID: {result.data.get('sys_id')})")
154 |         print(f"Description: {result.data.get('description', 'N/A')}")
155 |         print(f"Category: {result.data.get('category', 'N/A')}")
156 |         print(f"Price: {result.data.get('price', 'N/A')}")
157 |         print(f"Delivery Time: {result.data.get('delivery_time', 'N/A')}")
158 |         print(f"Availability: {result.data.get('availability', 'N/A')}")
159 |         
160 |         # Print variables
161 |         variables = result.data.get("variables", [])
162 |         if variables:
163 |             print("\nVariables:")
164 |             for i, variable in enumerate(variables, 1):
165 |                 print(f"{i}. {variable.get('label')} ({variable.get('name')})")
166 |                 print(f"   Type: {variable.get('type')}")
167 |                 print(f"   Mandatory: {variable.get('mandatory')}")
168 |                 print(f"   Default Value: {variable.get('default_value', 'N/A')}")
169 |                 print()
170 |     else:
171 |         print(f"Error: {result.message}")
172 | 
173 | 
174 | if __name__ == "__main__":
175 |     main() 
```

--------------------------------------------------------------------------------
/config/tool_packages.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | # ServiceNow MCP Tool Packages Configuration
  2 | # Define sets of tools to be exposed by the MCP server based on roles/workflows.
  3 | # The MCP server loads tools based on the MCP_TOOL_PACKAGE environment variable.
  4 | # If MCP_TOOL_PACKAGE is not set, 'full' is loaded.
  5 | # If MCP_TOOL_PACKAGE is set to 'none', no tools are loaded (except list_tool_packages).
  6 | 
  7 | # --- Package Definitions ---
  8 | 
  9 | none: []
 10 | 
 11 | service_desk:
 12 |   # Incident Management
 13 |   - create_incident
 14 |   - update_incident
 15 |   - add_comment
 16 |   - resolve_incident
 17 |   - list_incidents
 18 |   - get_incident_by_number
 19 |   # User Lookup
 20 |   - get_user
 21 |   - list_users
 22 |   # Knowledge Lookup
 23 |   - get_article
 24 |   - list_articles
 25 | 
 26 | catalog_builder:
 27 |   # Core Catalog
 28 |   - list_catalogs
 29 |   - list_catalog_items
 30 |   - get_catalog_item
 31 |   - create_catalog_item
 32 |   - update_catalog_item
 33 |   # Categories
 34 |   - list_catalog_categories
 35 |   - create_catalog_category
 36 |   - update_catalog_category
 37 |   - move_catalog_items
 38 |   # Variables
 39 |   - create_catalog_item_variable
 40 |   - list_catalog_item_variables
 41 |   - update_catalog_item_variable
 42 |   - delete_catalog_item_variable
 43 |   - create_catalog_variable_choice
 44 |   # Basic Scripting (UI Policy & User Criteria)
 45 |   - create_ui_policy
 46 |   - create_ui_policy_action
 47 |   - create_user_criteria
 48 |   - create_user_criteria_condition
 49 |   # Optimization
 50 |   - get_optimization_recommendations
 51 | 
 52 | change_coordinator:
 53 |   # Change Lifecycle
 54 |   - create_change_request
 55 |   - update_change_request
 56 |   - list_change_requests
 57 |   - get_change_request_details
 58 |   # Tasks & Approval
 59 |   - add_change_task
 60 |   - submit_change_for_approval
 61 |   - approve_change
 62 |   - reject_change
 63 | 
 64 | knowledge_author:
 65 |   # KB Management
 66 |   - create_knowledge_base
 67 |   - list_knowledge_bases
 68 |   # KB Categories
 69 |   - create_category  # KB specific category tool
 70 |   - list_categories  # KB specific category tool
 71 |   # Article Lifecycle
 72 |   - create_article
 73 |   - update_article
 74 |   - publish_article
 75 |   - list_articles
 76 |   - get_article
 77 | 
 78 | platform_developer:
 79 |   # Script Includes
 80 |   - list_script_includes
 81 |   - get_script_include
 82 |   - create_script_include
 83 |   - update_script_include
 84 |   - delete_script_include
 85 |   - execute_script_include
 86 |   # Workflows
 87 |   - list_workflows
 88 |   - get_workflow_details
 89 |   - create_workflow
 90 |   - update_workflow
 91 |   - activate_workflow
 92 |   - deactivate_workflow
 93 |   - list_workflow_versions
 94 |   # Workflow Activities
 95 |   - add_workflow_activity
 96 |   - update_workflow_activity
 97 |   - delete_workflow_activity
 98 |   - reorder_workflow_activities
 99 |   - get_workflow_activities
100 |   # General UI Policies
101 |   - create_ui_policy
102 |   - create_ui_policy_action
103 |   # Changesets
104 |   - list_changesets
105 |   - get_changeset_details
106 |   - create_changeset
107 |   - update_changeset
108 |   - commit_changeset
109 |   - publish_changeset
110 |   - add_file_to_changeset
111 | 
112 | agile_management:
113 |   # Story Management
114 |   - create_story
115 |   - update_story
116 |   - list_stories
117 |   - list_story_dependencies
118 |   - create_story_dependency
119 |   - delete_story_dependency
120 |   # Epic Management
121 |   - create_epic
122 |   - update_epic
123 |   - list_epics
124 |   # Scrum Task Management
125 |   - create_scrum_task
126 |   - update_scrum_task
127 |   - list_scrum_tasks
128 |   # Project Management
129 |   - create_project
130 |   - update_project
131 |   - list_projects
132 | 
133 | system_administrator:
134 |   # User Management
135 |   - create_user
136 |   - update_user
137 |   - get_user
138 |   - list_users
139 |   # Group Management
140 |   - create_group
141 |   - update_group
142 |   - list_groups
143 |   - add_group_members
144 |   - remove_group_members
145 |   # System Logs
146 |   - list_syslog_entries
147 |   - get_syslog_entry
148 | 
149 | full:
150 |   # Incident Management
151 |   - create_incident
152 |   - update_incident
153 |   - add_comment
154 |   - resolve_incident
155 |   - list_incidents
156 |   - get_incident_by_number
157 |   # Catalog (Core)
158 |   - list_catalogs
159 |   - list_catalog_items
160 |   - get_catalog_item
161 |   - create_catalog_item
162 |   - update_catalog_item
163 |   # Catalog (Categories)
164 |   - list_catalog_categories
165 |   - create_catalog_category
166 |   - update_catalog_category
167 |   - move_catalog_items
168 |   # Catalog (Optimization)
169 |   - get_optimization_recommendations
170 |   # Catalog (Variables)
171 |   - create_catalog_item_variable
172 |   - list_catalog_item_variables
173 |   - update_catalog_item_variable
174 |   - delete_catalog_item_variable
175 |   - create_catalog_variable_choice
176 |   # Change Management
177 |   - create_change_request
178 |   - update_change_request
179 |   - list_change_requests
180 |   - get_change_request_details
181 |   - add_change_task
182 |   - submit_change_for_approval
183 |   - approve_change
184 |   - reject_change
185 |   # Workflow
186 |   - list_workflows
187 |   - get_workflow_details
188 |   - list_workflow_versions
189 |   - get_workflow_activities
190 |   - create_workflow
191 |   - update_workflow
192 |   - activate_workflow
193 |   - deactivate_workflow
194 |   - add_workflow_activity
195 |   - update_workflow_activity
196 |   - delete_workflow_activity
197 |   - reorder_workflow_activities
198 |   # Changeset
199 |   - list_changesets
200 |   - get_changeset_details
201 |   - create_changeset
202 |   - update_changeset
203 |   - commit_changeset
204 |   - publish_changeset
205 |   - add_file_to_changeset
206 |   # Script Include
207 |   - list_script_includes
208 |   - get_script_include
209 |   - create_script_include
210 |   - update_script_include
211 |   - delete_script_include
212 |   - execute_script_include
213 |   # Knowledge Base
214 |   - create_knowledge_base
215 |   - list_knowledge_bases
216 |   - create_category # KB specific
217 |   - list_categories # KB specific
218 |   - create_article
219 |   - update_article
220 |   - publish_article
221 |   - list_articles
222 |   - get_article
223 |   # User Management
224 |   - create_user
225 |   - update_user
226 |   - get_user
227 |   - list_users
228 |   - create_group
229 |   - update_group
230 |   - add_group_members
231 |   - remove_group_members
232 |   - list_groups
233 |   # Syslog
234 |   - list_syslog_entries
235 |   - get_syslog_entry
236 |   # Catalog Criteria
237 |   - create_user_criteria
238 |   - create_user_criteria_condition
239 |   # UI Policy
240 |   - create_ui_policy
241 |   - create_ui_policy_action
242 |   # Story Management
243 |   - create_story
244 |   - update_story
245 |   - list_stories
246 |   - list_story_dependencies
247 |   - create_story_dependency
248 |   - delete_story_dependency
249 |   # Epic Management
250 |   - create_epic
251 |   - update_epic
252 |   - list_epics
253 |   # Scrum Task Management
254 |   - create_scrum_task
255 |   - update_scrum_task
256 |   - list_scrum_tasks
257 |   # Project Management
258 |   - create_project
259 |   - update_project
260 |   - list_projects
```

--------------------------------------------------------------------------------
/docs/changeset_management.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Changeset Management in ServiceNow MCP
  2 | 
  3 | This document provides detailed information about the Changeset Management tools available in the ServiceNow MCP server.
  4 | 
  5 | ## Overview
  6 | 
  7 | Changesets in ServiceNow (also known as Update Sets) are collections of customizations and configurations that can be moved between ServiceNow instances. They allow developers to track changes, collaborate on development, and promote changes through different environments (development, test, production).
  8 | 
  9 | The ServiceNow MCP server provides tools for managing changesets, allowing Claude to help with:
 10 | 
 11 | - Tracking development changes
 12 | - Creating and managing changesets
 13 | - Committing and publishing changesets
 14 | - Adding files to changesets
 15 | - Analyzing changeset contents
 16 | 
 17 | ## Available Tools
 18 | 
 19 | ### 1. list_changesets
 20 | 
 21 | Lists changesets from ServiceNow with various filtering options.
 22 | 
 23 | **Parameters:**
 24 | - `limit` (optional, default: 10) - Maximum number of records to return
 25 | - `offset` (optional, default: 0) - Offset to start from
 26 | - `state` (optional) - Filter by state (e.g., "in_progress", "complete", "published")
 27 | - `application` (optional) - Filter by application
 28 | - `developer` (optional) - Filter by developer
 29 | - `timeframe` (optional) - Filter by timeframe ("recent", "last_week", "last_month")
 30 | - `query` (optional) - Additional query string
 31 | 
 32 | **Example:**
 33 | ```python
 34 | result = list_changesets({
 35 |     "limit": 20,
 36 |     "state": "in_progress",
 37 |     "developer": "john.doe",
 38 |     "timeframe": "recent"
 39 | })
 40 | ```
 41 | 
 42 | ### 2. get_changeset_details
 43 | 
 44 | Gets detailed information about a specific changeset, including all changes contained within it.
 45 | 
 46 | **Parameters:**
 47 | - `changeset_id` (required) - Changeset ID or sys_id
 48 | 
 49 | **Example:**
 50 | ```python
 51 | result = get_changeset_details({
 52 |     "changeset_id": "sys_update_set_123"
 53 | })
 54 | ```
 55 | 
 56 | ### 3. create_changeset
 57 | 
 58 | Creates a new changeset in ServiceNow.
 59 | 
 60 | **Parameters:**
 61 | - `name` (required) - Name of the changeset
 62 | - `description` (optional) - Description of the changeset
 63 | - `application` (required) - Application the changeset belongs to
 64 | - `developer` (optional) - Developer responsible for the changeset
 65 | 
 66 | **Example:**
 67 | ```python
 68 | result = create_changeset({
 69 |     "name": "HR Portal Login Fix",
 70 |     "description": "Fixes the login issue on the HR Portal",
 71 |     "application": "HR Portal",
 72 |     "developer": "john.doe"
 73 | })
 74 | ```
 75 | 
 76 | ### 4. update_changeset
 77 | 
 78 | Updates an existing changeset in ServiceNow.
 79 | 
 80 | **Parameters:**
 81 | - `changeset_id` (required) - Changeset ID or sys_id
 82 | - `name` (optional) - Name of the changeset
 83 | - `description` (optional) - Description of the changeset
 84 | - `state` (optional) - State of the changeset
 85 | - `developer` (optional) - Developer responsible for the changeset
 86 | 
 87 | **Example:**
 88 | ```python
 89 | result = update_changeset({
 90 |     "changeset_id": "sys_update_set_123",
 91 |     "name": "HR Portal Login Fix - Updated",
 92 |     "description": "Updated description for the login fix",
 93 |     "state": "in_progress"
 94 | })
 95 | ```
 96 | 
 97 | ### 5. commit_changeset
 98 | 
 99 | Commits a changeset in ServiceNow, marking it as complete.
100 | 
101 | **Parameters:**
102 | - `changeset_id` (required) - Changeset ID or sys_id
103 | - `commit_message` (optional) - Commit message
104 | 
105 | **Example:**
106 | ```python
107 | result = commit_changeset({
108 |     "changeset_id": "sys_update_set_123",
109 |     "commit_message": "Completed the login fix with all necessary changes"
110 | })
111 | ```
112 | 
113 | ### 6. publish_changeset
114 | 
115 | Publishes a changeset in ServiceNow, making it available for deployment to other environments.
116 | 
117 | **Parameters:**
118 | - `changeset_id` (required) - Changeset ID or sys_id
119 | - `publish_notes` (optional) - Notes for publishing
120 | 
121 | **Example:**
122 | ```python
123 | result = publish_changeset({
124 |     "changeset_id": "sys_update_set_123",
125 |     "publish_notes": "Ready for deployment to test environment"
126 | })
127 | ```
128 | 
129 | ### 7. add_file_to_changeset
130 | 
131 | Adds a file to a changeset in ServiceNow.
132 | 
133 | **Parameters:**
134 | - `changeset_id` (required) - Changeset ID or sys_id
135 | - `file_path` (required) - Path of the file to add
136 | - `file_content` (required) - Content of the file
137 | 
138 | **Example:**
139 | ```python
140 | result = add_file_to_changeset({
141 |     "changeset_id": "sys_update_set_123",
142 |     "file_path": "scripts/login_fix.js",
143 |     "file_content": "function fixLogin() { ... }"
144 | })
145 | ```
146 | 
147 | ## Resources
148 | 
149 | The ServiceNow MCP server also provides the following resources for accessing changesets:
150 | 
151 | ### 1. changesets://list
152 | 
153 | URI for listing changesets from ServiceNow.
154 | 
155 | **Example:**
156 | ```
157 | changesets://list
158 | ```
159 | 
160 | ### 2. changeset://{changeset_id}
161 | 
162 | URI for getting a specific changeset from ServiceNow by ID.
163 | 
164 | **Example:**
165 | ```
166 | changeset://sys_update_set_123
167 | ```
168 | 
169 | ## Changeset States
170 | 
171 | Changesets in ServiceNow typically go through the following states:
172 | 
173 | 1. **in_progress** - The changeset is being actively worked on
174 | 2. **complete** - The changeset has been completed and is ready for review
175 | 3. **published** - The changeset has been published and is ready for deployment
176 | 4. **deployed** - The changeset has been deployed to another environment
177 | 
178 | ## Best Practices
179 | 
180 | 1. **Naming Convention**: Use a consistent naming convention for changesets that includes the application name, feature/fix description, and optionally a ticket number.
181 | 
182 | 2. **Scope**: Keep changesets focused on a single feature, fix, or improvement to make them easier to review and deploy.
183 | 
184 | 3. **Documentation**: Include detailed descriptions for changesets to help reviewers understand the purpose and impact of the changes.
185 | 
186 | 4. **Testing**: Test all changes thoroughly before committing and publishing a changeset.
187 | 
188 | 5. **Review**: Have changesets reviewed by another developer before publishing to catch potential issues.
189 | 
190 | 6. **Backup**: Always back up important configurations before deploying changesets to production.
191 | 
192 | ## Example Workflow
193 | 
194 | 1. Create a new changeset for a specific feature or fix
195 | 2. Make the necessary changes in ServiceNow
196 | 3. Add any required files to the changeset
197 | 4. Test the changes thoroughly
198 | 5. Commit the changeset with a detailed message
199 | 6. Have the changeset reviewed
200 | 7. Publish the changeset
201 | 8. Deploy the changeset to the target environment
202 | 
203 | ## Troubleshooting
204 | 
205 | ### Common Issues
206 | 
207 | 1. **Changeset Conflicts**: When multiple developers modify the same configuration item, conflicts can occur. Resolve these by carefully reviewing and merging the changes.
208 | 
209 | 2. **Missing Dependencies**: Changesets may depend on other configurations that aren't included. Ensure all dependencies are identified and included.
210 | 
211 | 3. **Deployment Failures**: If a changeset fails to deploy, check the deployment logs for specific errors and address them before retrying.
212 | 
213 | 4. **Permission Issues**: Ensure the user has the necessary permissions to create, commit, and publish changesets.
214 | 
215 | ### Error Messages
216 | 
217 | - **"Cannot find changeset"**: The specified changeset ID does not exist or is not accessible.
218 | - **"Missing required fields"**: One or more required parameters are missing.
219 | - **"Invalid state transition"**: Attempting to change the state of a changeset in an invalid way (e.g., from "in_progress" directly to "published").
220 | - **"Application not found"**: The specified application does not exist or is not accessible. 
```

--------------------------------------------------------------------------------
/scripts/check_pdi_info.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python
  2 | """
  3 | ServiceNow PDI Information Checker
  4 | 
  5 | This script helps determine the correct credentials for a ServiceNow PDI instance.
  6 | It will:
  7 | 1. Check if the instance is reachable
  8 | 2. Try different common username combinations
  9 | 3. Provide guidance on finding the correct credentials
 10 | 
 11 | Usage:
 12 |     python scripts/check_pdi_info.py
 13 | """
 14 | 
 15 | import os
 16 | import sys
 17 | import requests
 18 | from pathlib import Path
 19 | from getpass import getpass
 20 | from dotenv import load_dotenv
 21 | 
 22 | # Add the project root to the Python path
 23 | sys.path.insert(0, str(Path(__file__).parent.parent))
 24 | 
 25 | def check_instance_info(instance_url):
 26 |     """Check basic information about the ServiceNow instance."""
 27 |     print(f"\nChecking instance: {instance_url}")
 28 |     
 29 |     # Check if the instance is reachable
 30 |     try:
 31 |         response = requests.get(f"{instance_url}/api/now/table/sys_properties?sysparm_limit=1", 
 32 |                                headers={"Accept": "application/json"})
 33 |         
 34 |         if response.status_code == 200:
 35 |             print("✅ Instance is reachable")
 36 |             print("❌ But authentication is required")
 37 |         elif response.status_code == 401:
 38 |             print("✅ Instance is reachable")
 39 |             print("❌ Authentication required")
 40 |         else:
 41 |             print(f"❌ Instance returned unexpected status code: {response.status_code}")
 42 |             print(f"Response: {response.text}")
 43 |     except requests.exceptions.RequestException as e:
 44 |         print(f"❌ Error connecting to instance: {e}")
 45 |         return False
 46 |     
 47 |     return True
 48 | 
 49 | def test_credentials(instance_url, username, password):
 50 |     """Test a set of credentials against the ServiceNow instance."""
 51 |     print(f"\nTesting credentials: {username} / {'*' * len(password)}")
 52 |     
 53 |     try:
 54 |         response = requests.get(
 55 |             f"{instance_url}/api/now/table/incident?sysparm_limit=1",
 56 |             auth=(username, password),
 57 |             headers={"Accept": "application/json"}
 58 |         )
 59 |         
 60 |         if response.status_code == 200:
 61 |             print("✅ Authentication successful!")
 62 |             data = response.json()
 63 |             print(f"Retrieved {len(data.get('result', []))} incident(s)")
 64 |             return True
 65 |         else:
 66 |             print(f"❌ Authentication failed with status code: {response.status_code}")
 67 |             print(f"Response: {response.text}")
 68 |             return False
 69 |     except requests.exceptions.RequestException as e:
 70 |         print(f"❌ Connection error: {e}")
 71 |         return False
 72 | 
 73 | def main():
 74 |     """Main function to run the PDI checker."""
 75 |     load_dotenv()
 76 |     
 77 |     print("=" * 60)
 78 |     print("ServiceNow PDI Credential Checker".center(60))
 79 |     print("=" * 60)
 80 |     
 81 |     # Get instance URL
 82 |     instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
 83 |     if not instance_url or instance_url == "https://your-instance.service-now.com":
 84 |         instance_url = input("Enter your ServiceNow instance URL: ")
 85 |     
 86 |     # Check if instance is reachable
 87 |     if not check_instance_info(instance_url):
 88 |         print("\nPlease check your instance URL and try again.")
 89 |         return
 90 |     
 91 |     print("\nFor a Personal Developer Instance (PDI), try these common usernames:")
 92 |     print("1. admin")
 93 |     print("2. Your ServiceNow account email")
 94 |     print("3. Your ServiceNow username (without @domain.com)")
 95 |     
 96 |     # Try with current credentials first
 97 |     current_username = os.getenv("SERVICENOW_USERNAME")
 98 |     current_password = os.getenv("SERVICENOW_PASSWORD")
 99 |     
100 |     if current_username and current_password:
101 |         print("\nTrying with current credentials from .env file...")
102 |         if test_credentials(instance_url, current_username, current_password):
103 |             print("\n✅ Your current credentials in .env are working!")
104 |             return
105 |     
106 |     # Ask for new credentials
107 |     print("\nLet's try with new credentials:")
108 |     
109 |     # Try admin
110 |     print("\nTrying with 'admin' username...")
111 |     admin_password = getpass("Enter password for 'admin' user (press Enter to skip): ")
112 |     if admin_password and test_credentials(instance_url, "admin", admin_password):
113 |         update = input("\nDo you want to update your .env file with these credentials? (y/n): ")
114 |         if update.lower() == 'y':
115 |             update_env_file(instance_url, "admin", admin_password)
116 |         return
117 |     
118 |     # Try with email
119 |     email = input("\nEnter your ServiceNow account email: ")
120 |     if email:
121 |         email_password = getpass(f"Enter password for '{email}': ")
122 |         if test_credentials(instance_url, email, email_password):
123 |             update = input("\nDo you want to update your .env file with these credentials? (y/n): ")
124 |             if update.lower() == 'y':
125 |                 update_env_file(instance_url, email, email_password)
126 |             return
127 |     
128 |     # Try with username only (no domain)
129 |     if '@' in email:
130 |         username = email.split('@')[0]
131 |         print(f"\nTrying with username part only: '{username}'...")
132 |         username_password = getpass(f"Enter password for '{username}': ")
133 |         if test_credentials(instance_url, username, username_password):
134 |             update = input("\nDo you want to update your .env file with these credentials? (y/n): ")
135 |             if update.lower() == 'y':
136 |                 update_env_file(instance_url, username, username_password)
137 |             return
138 |     
139 |     print("\n❌ None of the common credential combinations worked.")
140 |     print("\nTo find your PDI credentials:")
141 |     print("1. Go to https://developer.servicenow.com/")
142 |     print("2. Log in with your ServiceNow account")
143 |     print("3. Go to 'My Instances'")
144 |     print("4. Find your PDI and click on it")
145 |     print("5. Look for the credentials information")
146 |     
147 |     # Ask for custom credentials
148 |     custom = input("\nDo you want to try custom credentials? (y/n): ")
149 |     if custom.lower() == 'y':
150 |         custom_username = input("Enter username: ")
151 |         custom_password = getpass("Enter password: ")
152 |         if test_credentials(instance_url, custom_username, custom_password):
153 |             update = input("\nDo you want to update your .env file with these credentials? (y/n): ")
154 |             if update.lower() == 'y':
155 |                 update_env_file(instance_url, custom_username, custom_password)
156 | 
157 | def update_env_file(instance_url, username, password):
158 |     """Update the .env file with working credentials."""
159 |     env_path = Path(__file__).parent.parent / '.env'
160 |     with open(env_path, 'r') as f:
161 |         env_content = f.read()
162 |     
163 |     # Update credentials
164 |     env_content = env_content.replace(f'SERVICENOW_INSTANCE_URL={os.getenv("SERVICENOW_INSTANCE_URL", "https://your-instance.service-now.com")}', f'SERVICENOW_INSTANCE_URL={instance_url}')
165 |     env_content = env_content.replace(f'SERVICENOW_USERNAME={os.getenv("SERVICENOW_USERNAME", "your-username")}', f'SERVICENOW_USERNAME={username}')
166 |     env_content = env_content.replace(f'SERVICENOW_PASSWORD={os.getenv("SERVICENOW_PASSWORD", "your-password")}', f'SERVICENOW_PASSWORD={password}')
167 |     
168 |     with open(env_path, 'w') as f:
169 |         f.write(env_content)
170 |     
171 |     print("✅ Updated .env file with working credentials!")
172 | 
173 | if __name__ == "__main__":
174 |     main() 
```

--------------------------------------------------------------------------------
/docs/catalog_variables.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Catalog Item Variables in ServiceNow MCP
  2 | 
  3 | This document describes the tools available for managing variables (form fields) in ServiceNow catalog items using the ServiceNow MCP server.
  4 | 
  5 | ## Overview
  6 | 
  7 | Catalog item variables are the form fields that users fill out when ordering items from the service catalog. They collect information needed to fulfill service requests. These tools allow you to create, list, and update variables for catalog items.
  8 | 
  9 | ## Available Tools
 10 | 
 11 | ### create_catalog_item_variable
 12 | 
 13 | Creates a new variable (form field) for a catalog item.
 14 | 
 15 | #### Parameters
 16 | 
 17 | | Parameter | Type | Required | Description |
 18 | |-----------|------|----------|-------------|
 19 | | catalog_item_id | string | Yes | The sys_id of the catalog item |
 20 | | name | string | Yes | The name of the variable (internal name) |
 21 | | type | string | Yes | The type of variable (e.g., string, integer, boolean, reference) |
 22 | | label | string | Yes | The display label for the variable |
 23 | | mandatory | boolean | No | Whether the variable is required (default: false) |
 24 | | help_text | string | No | Help text to display with the variable |
 25 | | default_value | string | No | Default value for the variable |
 26 | | description | string | No | Description of the variable |
 27 | | order | integer | No | Display order of the variable |
 28 | | reference_table | string | No | For reference fields, the table to reference |
 29 | | reference_qualifier | string | No | For reference fields, the query to filter reference options |
 30 | | max_length | integer | No | Maximum length for string fields |
 31 | | min | integer | No | Minimum value for numeric fields |
 32 | | max | integer | No | Maximum value for numeric fields |
 33 | 
 34 | #### Example
 35 | 
 36 | ```python
 37 | result = create_catalog_item_variable({
 38 |     "catalog_item_id": "item123",
 39 |     "name": "requested_for",
 40 |     "type": "reference",
 41 |     "label": "Requested For",
 42 |     "mandatory": true,
 43 |     "reference_table": "sys_user",
 44 |     "reference_qualifier": "active=true",
 45 |     "help_text": "User who needs this item"
 46 | })
 47 | ```
 48 | 
 49 | #### Response
 50 | 
 51 | | Field | Type | Description |
 52 | |-------|------|-------------|
 53 | | success | boolean | Whether the operation was successful |
 54 | | message | string | A message describing the result |
 55 | | variable_id | string | The sys_id of the created variable |
 56 | | details | object | Additional details about the variable |
 57 | 
 58 | ### list_catalog_item_variables
 59 | 
 60 | Lists all variables (form fields) for a catalog item.
 61 | 
 62 | #### Parameters
 63 | 
 64 | | Parameter | Type | Required | Description |
 65 | |-----------|------|----------|-------------|
 66 | | catalog_item_id | string | Yes | The sys_id of the catalog item |
 67 | | include_details | boolean | No | Whether to include detailed information about each variable (default: true) |
 68 | | limit | integer | No | Maximum number of variables to return |
 69 | | offset | integer | No | Offset for pagination |
 70 | 
 71 | #### Example
 72 | 
 73 | ```python
 74 | result = list_catalog_item_variables({
 75 |     "catalog_item_id": "item123",
 76 |     "include_details": true,
 77 |     "limit": 10,
 78 |     "offset": 0
 79 | })
 80 | ```
 81 | 
 82 | #### Response
 83 | 
 84 | | Field | Type | Description |
 85 | |-------|------|-------------|
 86 | | success | boolean | Whether the operation was successful |
 87 | | message | string | A message describing the result |
 88 | | variables | array | List of variables |
 89 | | count | integer | Total number of variables found |
 90 | 
 91 | ### update_catalog_item_variable
 92 | 
 93 | Updates an existing variable (form field) for a catalog item.
 94 | 
 95 | #### Parameters
 96 | 
 97 | | Parameter | Type | Required | Description |
 98 | |-----------|------|----------|-------------|
 99 | | variable_id | string | Yes | The sys_id of the variable to update |
100 | | label | string | No | The display label for the variable |
101 | | mandatory | boolean | No | Whether the variable is required |
102 | | help_text | string | No | Help text to display with the variable |
103 | | default_value | string | No | Default value for the variable |
104 | | description | string | No | Description of the variable |
105 | | order | integer | No | Display order of the variable |
106 | | reference_qualifier | string | No | For reference fields, the query to filter reference options |
107 | | max_length | integer | No | Maximum length for string fields |
108 | | min | integer | No | Minimum value for numeric fields |
109 | | max | integer | No | Maximum value for numeric fields |
110 | 
111 | #### Example
112 | 
113 | ```python
114 | result = update_catalog_item_variable({
115 |     "variable_id": "var123",
116 |     "label": "Updated Label",
117 |     "mandatory": true,
118 |     "help_text": "New help text",
119 |     "order": 200
120 | })
121 | ```
122 | 
123 | #### Response
124 | 
125 | | Field | Type | Description |
126 | |-------|------|-------------|
127 | | success | boolean | Whether the operation was successful |
128 | | message | string | A message describing the result |
129 | | variable_id | string | The sys_id of the updated variable |
130 | | details | object | Additional details about the variable |
131 | 
132 | ## Common Variable Types
133 | 
134 | ServiceNow supports various variable types:
135 | 
136 | | Type | Description |
137 | |------|-------------|
138 | | string | Single-line text field |
139 | | multi_line_text | Multi-line text area |
140 | | integer | Whole number field |
141 | | float | Decimal number field |
142 | | boolean | Checkbox (true/false) |
143 | | choice | Dropdown menu or radio buttons |
144 | | reference | Reference to another record |
145 | | date | Date picker |
146 | | datetime | Date and time picker |
147 | | password | Password field (masked input) |
148 | | email | Email address field |
149 | | url | URL field |
150 | | currency | Currency field |
151 | | html | Rich text editor |
152 | | upload | File attachment |
153 | 
154 | ## Example Usage with Claude
155 | 
156 | Once the ServiceNow MCP server is configured with Claude, you can ask Claude to perform actions like:
157 | 
158 | - "Create a description field for the laptop request catalog item"
159 | - "Add a dropdown field for selecting laptop models to catalog item sys_id_123"
160 | - "List all variables for the VPN access request catalog item"
161 | - "Make the department field mandatory in the software request form"
162 | - "Update the help text for the cost center field in the travel request form"
163 | - "Add a reference field to the computer request form that lets users select their manager"
164 | - "Show all variables for the new hire onboarding catalog item in order"
165 | - "Change the order of fields in the hardware request form to put name first"
166 | 
167 | ## Troubleshooting
168 | 
169 | ### Common Errors
170 | 
171 | 1. **Error: `Mandatory variable not provided`**
172 |    - This error occurs when you don't provide all required parameters when creating a variable.
173 |    - Solution: Make sure to include all required parameters: `catalog_item_id`, `name`, `type`, and `label`.
174 | 
175 | 2. **Error: `Invalid variable type specified`**
176 |    - This error occurs when you provide an invalid value for the `type` parameter.
177 |    - Solution: Use one of the valid variable types listed in the "Common Variable Types" section.
178 | 
179 | 3. **Error: `Reference table required for reference variables`**
180 |    - This error occurs when creating a reference-type variable without specifying the `reference_table`.
181 |    - Solution: Always include the `reference_table` parameter when creating reference-type variables.
182 | 
183 | 4. **Error: `No update parameters provided`**
184 |    - This error occurs when calling `update_catalog_item_variable` with only the variable_id and no other parameters.
185 |    - Solution: Provide at least one field to update. 
```

--------------------------------------------------------------------------------
/tests/test_workflow_tools_params.py:
--------------------------------------------------------------------------------

```python
  1 | import unittest
  2 | from unittest.mock import MagicMock, patch
  3 | 
  4 | from servicenow_mcp.auth.auth_manager import AuthManager
  5 | from servicenow_mcp.utils.config import ServerConfig
  6 | from servicenow_mcp.tools.workflow_tools import (
  7 |     _get_auth_and_config,
  8 |     list_workflows,
  9 |     get_workflow_details,
 10 |     create_workflow,
 11 | )
 12 | 
 13 | 
 14 | class TestWorkflowToolsParams(unittest.TestCase):
 15 |     """Test parameter handling in workflow tools."""
 16 | 
 17 |     def setUp(self):
 18 |         """Set up test fixtures."""
 19 |         # Create mock objects for AuthManager and ServerConfig
 20 |         self.auth_manager = MagicMock(spec=AuthManager)
 21 |         self.auth_manager.get_headers.return_value = {"Authorization": "Bearer test-token"}
 22 |         
 23 |         self.server_config = MagicMock(spec=ServerConfig)
 24 |         self.server_config.instance_url = "https://test-instance.service-now.com"
 25 | 
 26 |     def test_get_auth_and_config_correct_order(self):
 27 |         """Test _get_auth_and_config with parameters in the correct order."""
 28 |         auth, config = _get_auth_and_config(self.auth_manager, self.server_config)
 29 |         self.assertEqual(auth, self.auth_manager)
 30 |         self.assertEqual(config, self.server_config)
 31 | 
 32 |     def test_get_auth_and_config_swapped_order(self):
 33 |         """Test _get_auth_and_config with parameters in the swapped order."""
 34 |         auth, config = _get_auth_and_config(self.server_config, self.auth_manager)
 35 |         self.assertEqual(auth, self.auth_manager)
 36 |         self.assertEqual(config, self.server_config)
 37 | 
 38 |     def test_get_auth_and_config_error_handling(self):
 39 |         """Test _get_auth_and_config error handling with invalid parameters."""
 40 |         # Create objects that don't have the required attributes
 41 |         invalid_obj1 = MagicMock()
 42 |         # Explicitly remove attributes to ensure they don't exist
 43 |         del invalid_obj1.get_headers
 44 |         del invalid_obj1.instance_url
 45 |         
 46 |         invalid_obj2 = MagicMock()
 47 |         # Explicitly remove attributes to ensure they don't exist
 48 |         del invalid_obj2.get_headers
 49 |         del invalid_obj2.instance_url
 50 |         
 51 |         with self.assertRaises(ValueError):
 52 |             _get_auth_and_config(invalid_obj1, invalid_obj2)
 53 | 
 54 |     @patch('servicenow_mcp.tools.workflow_tools.requests.get')
 55 |     def test_list_workflows_correct_params(self, mock_get):
 56 |         """Test list_workflows with parameters in the correct order."""
 57 |         # Setup mock response
 58 |         mock_response = MagicMock()
 59 |         mock_response.json.return_value = {"result": [{"sys_id": "123", "name": "Test Workflow"}]}
 60 |         mock_response.headers = {"X-Total-Count": "1"}
 61 |         mock_get.return_value = mock_response
 62 |         
 63 |         # Call the function
 64 |         result = list_workflows(self.auth_manager, self.server_config, {"limit": 10})
 65 |         
 66 |         # Verify the function called requests.get with the correct parameters
 67 |         mock_get.assert_called_once()
 68 |         self.assertEqual(result["count"], 1)
 69 |         self.assertEqual(result["workflows"][0]["name"], "Test Workflow")
 70 | 
 71 |     @patch('servicenow_mcp.tools.workflow_tools.requests.get')
 72 |     def test_list_workflows_swapped_params(self, mock_get):
 73 |         """Test list_workflows with parameters in the swapped order."""
 74 |         # Setup mock response
 75 |         mock_response = MagicMock()
 76 |         mock_response.json.return_value = {"result": [{"sys_id": "123", "name": "Test Workflow"}]}
 77 |         mock_response.headers = {"X-Total-Count": "1"}
 78 |         mock_get.return_value = mock_response
 79 |         
 80 |         # Call the function with swapped parameters
 81 |         result = list_workflows(self.server_config, self.auth_manager, {"limit": 10})
 82 |         
 83 |         # Verify the function still works correctly
 84 |         mock_get.assert_called_once()
 85 |         self.assertEqual(result["count"], 1)
 86 |         self.assertEqual(result["workflows"][0]["name"], "Test Workflow")
 87 | 
 88 |     @patch('servicenow_mcp.tools.workflow_tools.requests.get')
 89 |     def test_get_workflow_details_correct_params(self, mock_get):
 90 |         """Test get_workflow_details with parameters in the correct order."""
 91 |         # Setup mock response
 92 |         mock_response = MagicMock()
 93 |         mock_response.json.return_value = {"result": {"sys_id": "123", "name": "Test Workflow"}}
 94 |         mock_get.return_value = mock_response
 95 |         
 96 |         # Call the function
 97 |         result = get_workflow_details(self.auth_manager, self.server_config, {"workflow_id": "123"})
 98 |         
 99 |         # Verify the function called requests.get with the correct parameters
100 |         mock_get.assert_called_once()
101 |         self.assertEqual(result["workflow"]["name"], "Test Workflow")
102 | 
103 |     @patch('servicenow_mcp.tools.workflow_tools.requests.get')
104 |     def test_get_workflow_details_swapped_params(self, mock_get):
105 |         """Test get_workflow_details with parameters in the swapped order."""
106 |         # Setup mock response
107 |         mock_response = MagicMock()
108 |         mock_response.json.return_value = {"result": {"sys_id": "123", "name": "Test Workflow"}}
109 |         mock_get.return_value = mock_response
110 |         
111 |         # Call the function with swapped parameters
112 |         result = get_workflow_details(self.server_config, self.auth_manager, {"workflow_id": "123"})
113 |         
114 |         # Verify the function still works correctly
115 |         mock_get.assert_called_once()
116 |         self.assertEqual(result["workflow"]["name"], "Test Workflow")
117 | 
118 |     @patch('servicenow_mcp.tools.workflow_tools.requests.post')
119 |     def test_create_workflow_correct_params(self, mock_post):
120 |         """Test create_workflow with parameters in the correct order."""
121 |         # Setup mock response
122 |         mock_response = MagicMock()
123 |         mock_response.json.return_value = {"result": {"sys_id": "123", "name": "New Workflow"}}
124 |         mock_post.return_value = mock_response
125 |         
126 |         # Call the function
127 |         result = create_workflow(
128 |             self.auth_manager, 
129 |             self.server_config, 
130 |             {"name": "New Workflow", "description": "Test description"}
131 |         )
132 |         
133 |         # Verify the function called requests.post with the correct parameters
134 |         mock_post.assert_called_once()
135 |         self.assertEqual(result["workflow"]["name"], "New Workflow")
136 |         self.assertEqual(result["message"], "Workflow created successfully")
137 | 
138 |     @patch('servicenow_mcp.tools.workflow_tools.requests.post')
139 |     def test_create_workflow_swapped_params(self, mock_post):
140 |         """Test create_workflow with parameters in the swapped order."""
141 |         # Setup mock response
142 |         mock_response = MagicMock()
143 |         mock_response.json.return_value = {"result": {"sys_id": "123", "name": "New Workflow"}}
144 |         mock_post.return_value = mock_response
145 |         
146 |         # Call the function with swapped parameters
147 |         result = create_workflow(
148 |             self.server_config, 
149 |             self.auth_manager, 
150 |             {"name": "New Workflow", "description": "Test description"}
151 |         )
152 |         
153 |         # Verify the function still works correctly
154 |         mock_post.assert_called_once()
155 |         self.assertEqual(result["workflow"]["name"], "New Workflow")
156 |         self.assertEqual(result["message"], "Workflow created successfully")
157 | 
158 | 
159 | if __name__ == '__main__':
160 |     unittest.main() 
```
Page 1/5FirstPrevNextLast