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 | [](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()
```