This is page 1 of 4. Use http://codebase.md/osomai/servicenow-mcp?lines=false&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:
--------------------------------------------------------------------------------
```
SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com
SERVICENOW_USERNAME=your-username
SERVICENOW_PASSWORD=your-password
```
--------------------------------------------------------------------------------
/.cursorrules:
--------------------------------------------------------------------------------
```
# Python project structure rules
# This file defines the structure for a Python project with similar organization to the ServiceNow MCP project
# Root directory structure
root/
├── src/ # Source code directory
│ └── servicenow_mcp/ # Main package directory
│ ├── __init__.py # Package initialization
│ ├── server.py # Server implementation
│ ├── cli.py # CLI implementation
│ ├── auth/ # Authentication related code
│ ├── tools/ # Tool implementations
│ ├── utils/ # Utility functions
│ └── resources/ # Resource definitions
├── tests/ # Test directory
│ ├── __init__.py
│ └── test_*.py # Test files
├── docs/ # Documentation
├── examples/ # Example code
├── scripts/ # Utility scripts
├── .env # Environment variables
├── .gitignore # Git ignore rules
├── LICENSE # License file
├── README.md # Project documentation
├── pyproject.toml # Project configuration
└── uv.lock # Dependency lock file
# File naming conventions
*.py:
- Use snake_case for file names
- Test files should start with test_
- Main package files should be descriptive of their purpose
# Directory naming conventions
directories:
- Use snake_case for directory names
- Keep directory names lowercase
- Use plural form for directories containing multiple items
# Import structure
imports:
- Group imports in the following order:
1. Standard library imports
2. Third-party imports
3. Local application imports
- Use absolute imports for external packages
- Use relative imports for local modules
# Testing conventions
tests:
- Each test file should correspond to a module in the source code
- Test classes should be prefixed with Test
- Test methods should be prefixed with test_
- Use pytest fixtures for common setup
# Documentation
docs:
- Include docstrings for all public functions and classes
- Use Google style docstrings
- Keep README.md up to date with project information
- Document API endpoints and usage in docs/
# Code style
style:
- Follow PEP 8 guidelines
- Use type hints for function parameters and return values
- Keep functions focused and single-purpose
- Use meaningful variable and function names
- Add comments for complex logic
# Version control
git:
- Use meaningful commit messages
- Keep commits focused and atomic
- Include relevant issue numbers in commit messages
- Use feature branches for new development
# Dependencies
dependencies:
- Use pyproject.toml for project configuration
- Keep dependencies up to date
- Use uv.lock for deterministic builds
- Document all external dependencies in README.md
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# PyPI configuration file
.pypirc
# IDE files
.idea/
.vscode/
*.swp
*.swo
# Project specific
.env.local
.env.development
.env.test
.env.production
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
[](https://mseep.ai/app/osomai-servicenow-mcp)
# ServiceNow MCP Server
A Model Completion Protocol (MCP) server implementation for ServiceNow, allowing Claude to interact with ServiceNow instances.
<a href="https://glama.ai/mcp/servers/@osomai/servicenow-mcp">
<img width="380" height="200" src="https://glama.ai/mcp/servers/@osomai/servicenow-mcp/badge" alt="ServiceNow Server MCP server" />
</a>
## Overview
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.
## Features
- Connect to ServiceNow instances using various authentication methods (Basic, OAuth, API Key)
- Query ServiceNow records and tables
- Create, update, and delete ServiceNow records
- Execute ServiceNow scripts and workflows
- Access and query the ServiceNow Service Catalog
- Analyze and optimize the ServiceNow Service Catalog
- Debug mode for troubleshooting
- Support for both stdio and Server-Sent Events (SSE) communication
## Installation
### Prerequisites
- Python 3.11 or higher
- A ServiceNow instance with appropriate access credentials
### Setup
1. Clone this repository:
```
git clone https://github.com/echelon-ai-labs/servicenow-mcp.git
cd servicenow-mcp
```
2. Create a virtual environment and install the package:
```
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -e .
```
3. Create a `.env` file with your ServiceNow credentials:
```
SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com
SERVICENOW_USERNAME=your-username
SERVICENOW_PASSWORD=your-password
SERVICENOW_AUTH_TYPE=basic # or oauth, api_key
```
## Usage
### Standard (stdio) Mode
To start the MCP server:
```
python -m servicenow_mcp.cli
```
Or with environment variables:
```
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
```
### Server-Sent Events (SSE) Mode
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.
#### Starting the SSE Server
You can start the SSE server using the provided CLI:
```
servicenow-mcp-sse --instance-url=https://your-instance.service-now.com --username=your-username --password=your-password
```
By default, the server will listen on `0.0.0.0:8080`. You can customize the host and port:
```
servicenow-mcp-sse --host=127.0.0.1 --port=8000
```
#### Connecting to the SSE Server
The SSE server exposes two main endpoints:
- `/sse` - The SSE connection endpoint
- `/messages/` - The endpoint for sending messages to the server
#### Example
See the `examples/sse_server_example.py` file for a complete example of setting up and running the SSE server.
```python
from servicenow_mcp.server import ServiceNowMCP
from servicenow_mcp.server_sse import create_starlette_app
from servicenow_mcp.utils.config import ServerConfig, AuthConfig, AuthType, BasicAuthConfig
import uvicorn
# Create server configuration
config = ServerConfig(
instance_url="https://your-instance.service-now.com",
auth=AuthConfig(
type=AuthType.BASIC,
config=BasicAuthConfig(
username="your-username",
password="your-password"
)
),
debug=True,
)
# Create ServiceNow MCP server
servicenow_mcp = ServiceNowMCP(config)
# Create Starlette app with SSE transport
app = create_starlette_app(servicenow_mcp, debug=True)
# Start the web server
uvicorn.run(app, host="0.0.0.0", port=8080)
```
## Tool Packaging (Optional)
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.
### Configuration
1. **Environment Variable:** Set the `MCP_TOOL_PACKAGE` environment variable to the name of the desired package.
```bash
export MCP_TOOL_PACKAGE=catalog_builder
```
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.
### Behavior
- 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.
- If `MCP_TOOL_PACKAGE` is **not set** or is empty, the `full` package (containing all tools) is loaded by default.
- 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.
- Setting `MCP_TOOL_PACKAGE=none` explicitly loads no tools (except `list_tool_packages`).
### Available Packages (Default)
The default `config/tool_packages.yaml` includes the following role-based packages:
- `service_desk`: Tools for incident handling and basic user/knowledge lookup.
- `catalog_builder`: Tools for creating and managing service catalog items, categories, variables, and related scripting (UI Policies, User Criteria).
- `change_coordinator`: Tools for managing the change request lifecycle, including tasks and approvals.
- `knowledge_author`: Tools for creating and managing knowledge bases, categories, and articles.
- `platform_developer`: Tools for server-side scripting (Script Includes), workflow development, and deployment (Changesets).
- `system_administrator`: Tools for user/group management and viewing system logs.
- `agile_management`: Tools for managing user stories, epics, scrum tasks, and projects.
- `full`: Includes all available tools (default).
- `none`: Includes no tools (except `list_tool_packages`).
### Introspection Tool
- **`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`.
## Available Tools
**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.
#### Incident Management Tools
1. **create_incident** - Create a new incident in ServiceNow
2. **update_incident** - Update an existing incident in ServiceNow
3. **add_comment** - Add a comment to an incident in ServiceNow
4. **resolve_incident** - Resolve an incident in ServiceNow
5. **list_incidents** - List incidents from ServiceNow
#### Service Catalog Tools
1. **list_catalog_items** - List service catalog items from ServiceNow
2. **get_catalog_item** - Get a specific service catalog item from ServiceNow
3. **list_catalog_categories** - List service catalog categories from ServiceNow
4. **create_catalog_category** - Create a new service catalog category in ServiceNow
5. **update_catalog_category** - Update an existing service catalog category in ServiceNow
6. **move_catalog_items** - Move catalog items between categories in ServiceNow
7. **create_catalog_item_variable** - Create a new variable (form field) for a catalog item
8. **list_catalog_item_variables** - List all variables for a catalog item
9. **update_catalog_item_variable** - Update an existing variable for a catalog item
10. **list_catalogs** - List service catalogs from ServiceNow
#### Catalog Optimization Tools
1. **get_optimization_recommendations** - Get recommendations for optimizing the service catalog
2. **update_catalog_item** - Update a service catalog item
#### Change Management Tools
1. **create_change_request** - Create a new change request in ServiceNow
2. **update_change_request** - Update an existing change request
3. **list_change_requests** - List change requests with filtering options
4. **get_change_request_details** - Get detailed information about a specific change request
5. **add_change_task** - Add a task to a change request
6. **submit_change_for_approval** - Submit a change request for approval
7. **approve_change** - Approve a change request
8. **reject_change** - Reject a change request
#### Agile Management Tools
##### Story Management
1. **create_story** - Create a new user story in ServiceNow
2. **update_story** - Update an existing user story in ServiceNow
3. **list_stories** - List user stories with filtering options
4. **create_story_dependency** - Create a dependency between two stories
5. **delete_story_dependency** - Delete a dependency between stories
##### Epic Management
1. **create_epic** - Create a new epic in ServiceNow
2. **update_epic** - Update an existing epic in ServiceNow
3. **list_epics** - List epics from ServiceNow with filtering options
##### Scrum Task Management
1. **create_scrum_task** - Create a new scrum task in ServiceNow
2. **update_scrum_task** - Update an existing scrum task in ServiceNow
3. **list_scrum_tasks** - List scrum tasks from ServiceNow with filtering options
##### Project Management
1. **create_project** - Create a new project in ServiceNow
2. **update_project** - Update an existing project in ServiceNow
3. **list_projects** - List projects from ServiceNow with filtering options
#### Workflow Management Tools
1. **list_workflows** - List workflows from ServiceNow
2. **get_workflow** - Get a specific workflow from ServiceNow
3. **create_workflow** - Create a new workflow in ServiceNow
4. **update_workflow** - Update an existing workflow in ServiceNow
5. **delete_workflow** - Delete a workflow from ServiceNow
#### Script Include Management Tools
1. **list_script_includes** - List script includes from ServiceNow
2. **get_script_include** - Get a specific script include from ServiceNow
3. **create_script_include** - Create a new script include in ServiceNow
4. **update_script_include** - Update an existing script include in ServiceNow
5. **delete_script_include** - Delete a script include from ServiceNow
#### Changeset Management Tools
1. **list_changesets** - List changesets from ServiceNow with filtering options
2. **get_changeset_details** - Get detailed information about a specific changeset
3. **create_changeset** - Create a new changeset in ServiceNow
4. **update_changeset** - Update an existing changeset
5. **commit_changeset** - Commit a changeset
6. **publish_changeset** - Publish a changeset
7. **add_file_to_changeset** - Add a file to a changeset
#### Knowledge Base Management Tools
1. **create_knowledge_base** - Create a new knowledge base in ServiceNow
2. **list_knowledge_bases** - List knowledge bases with filtering options
3. **create_category** - Create a new category in a knowledge base
4. **create_article** - Create a new knowledge article in ServiceNow
5. **update_article** - Update an existing knowledge article in ServiceNow
6. **publish_article** - Publish a knowledge article in ServiceNow
7. **list_articles** - List knowledge articles with filtering options
8. **get_article** - Get a specific knowledge article by ID
#### User Management Tools
1. **create_user** - Create a new user in ServiceNow
2. **update_user** - Update an existing user in ServiceNow
3. **get_user** - Get a specific user by ID, username, or email
4. **list_users** - List users with filtering options
5. **create_group** - Create a new group in ServiceNow
6. **update_group** - Update an existing group in ServiceNow
7. **add_group_members** - Add members to a group in ServiceNow
8. **remove_group_members** - Remove members from a group in ServiceNow
9. **list_groups** - List groups with filtering options
#### UI Policy Tools
1. **create_ui_policy** - Creates a ServiceNow UI Policy, typically for a Catalog Item.
2. **create_ui_policy_action** - Creates an action associated with a UI Policy to control variable states (visibility, mandatory, etc.).
### Using the MCP CLI
The ServiceNow MCP server can be installed with the MCP CLI, which provides a convenient way to register the server with Claude.
```bash
# Install the ServiceNow MCP server with environment variables from .env file
mcp install src/servicenow_mcp/server.py -f .env
```
This command will register the ServiceNow MCP server with Claude and configure it to use the environment variables from the .env file.
### Integration with Claude Desktop
To configure the ServiceNow MCP server in Claude Desktop:
1. Edit the Claude Desktop configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the appropriate path for your OS:
```json
{
"mcpServers": {
"ServiceNow": {
"command": "/Users/yourusername/dev/servicenow-mcp/.venv/bin/python",
"args": [
"-m",
"servicenow_mcp.cli"
],
"env": {
"SERVICENOW_INSTANCE_URL": "https://your-instance.service-now.com",
"SERVICENOW_USERNAME": "your-username",
"SERVICENOW_PASSWORD": "your-password",
"SERVICENOW_AUTH_TYPE": "basic"
}
}
}
}
```
2. Restart Claude Desktop to apply the changes
### Example Usage with Claude
Below are some example natural language queries you can use with Claude to interact with ServiceNow via the MCP server:
#### Incident Management Examples
- "Create a new incident for a network outage in the east region"
- "Update the priority of incident INC0010001 to high"
- "Add a comment to incident INC0010001 saying the issue is being investigated"
- "Resolve incident INC0010001 with a note that the server was restarted"
- "List all high priority incidents assigned to the Network team"
- "List all active P1 incidents assigned to the Network team."
#### Service Catalog Examples
- "Show me all items in the service catalog"
- "List all service catalog categories"
- "Get details about the laptop request catalog item"
- "Show me all catalog items in the Hardware category"
- "Search for 'software' in the service catalog"
- "Create a new category called 'Cloud Services' in the service catalog"
- "Update the 'Hardware' category to rename it to 'IT Equipment'"
- "Move the 'Virtual Machine' catalog item to the 'Cloud Services' category"
- "Create a subcategory called 'Monitors' under the 'IT Equipment' category"
- "Reorganize our catalog by moving all software items to the 'Software' category"
- "Create a description field for the laptop request catalog item"
- "Add a dropdown field for selecting laptop models to catalog item"
- "List all form fields for the VPN access request catalog item"
- "Make the department field mandatory in the software request form"
- "Update the help text for the cost center field"
- "Show me all service catalogs in the system"
- "List all hardware catalog items."
- "Find the catalog item for 'New Laptop Request'."
- "Show me the variables for the 'New Laptop Request' item."
- "Create a new variable named 'department_code' for the 'New Hire Setup' catalog item. Make it a mandatory string field."
#### Catalog Optimization Examples
- "Analyze our service catalog and identify opportunities for improvement"
- "Find catalog items with poor descriptions that need improvement"
- "Identify catalog items with low usage that we might want to retire"
- "Find catalog items with high abandonment rates"
- "Optimize our Hardware category to improve user experience"
#### Change Management Examples
- "Create a change request for server maintenance to apply security patches tomorrow night"
- "Schedule a database upgrade for next Tuesday from 2 AM to 4 AM"
- "Add a task to the server maintenance change for pre-implementation checks"
- "Submit the server maintenance change for approval"
- "Approve the database upgrade change with comment: implementation plan looks thorough"
- "Show me all emergency changes scheduled for this week"
- "List all changes assigned to the Network team"
- "Create a normal change request to upgrade the production database server."
- "Update change CHG0012345, set the state to 'Implement'."
#### Agile Management Examples
- "Create a new user story for implementing a new reporting dashboard"
- "Update the 'Implement a new reporting dashboard' story to set it as blocked"
- "List all user stories assigned to the Data Analytics team"
- "Create a dependency between the 'Implement a new reporting dashboard' story and the 'Develop data extraction pipeline' story"
- "Delete the dependency between the 'Implement a new reporting dashboard' story and the 'Develop data extraction pipeline' story"
- "Create a new epic called 'Data Analytics Initiatives'"
- "Update the 'Data Analytics Initiatives' epic to set it as completed"
- "List all epics in the 'Data Analytics' project"
- "Create a new scrum task for the 'Implement a new reporting dashboard' story"
- "Update the 'Develop data extraction pipeline' scrum task to set it as completed"
- "List all scrum tasks in the 'Implement a new reporting dashboard' story"
- "Create a new project called 'Data Analytics Initiatives'"
- "Update the 'Data Analytics Initiatives' project to set it as completed"
- "List all projects in the 'Data Analytics' epic"
#### Workflow Management Examples
- "Show me all active workflows in ServiceNow"
- "Get details about the incident approval workflow"
- "List all versions of the change request workflow"
- "Show me all activities in the service catalog request workflow"
- "Create a new workflow for handling software license requests"
- "Update the description of the incident escalation workflow"
- "Activate the new employee onboarding workflow"
- "Deactivate the old password reset workflow"
- "Add an approval activity to the software license request workflow"
- "Update the notification activity in the incident escalation workflow"
- "Delete the unnecessary activity from the change request workflow"
- "Reorder the activities in the service catalog request workflow"
#### Changeset Management Examples
- "List all changesets in ServiceNow"
- "Show me all changesets created by developer 'john.doe'"
- "Get details about changeset 'sys_update_set_123'"
- "Create a new changeset for the 'HR Portal' application"
- "Update the description of changeset 'sys_update_set_123'"
- "Commit changeset 'sys_update_set_123' with message 'Fixed login issue'"
- "Publish changeset 'sys_update_set_123' to production"
- "Add a file to changeset 'sys_update_set_123'"
- "Show me all changes in changeset 'sys_update_set_123'"
#### Knowledge Base Examples
- "Create a new knowledge base for the IT department"
- "List all knowledge bases in the organization"
- "Create a category called 'Network Troubleshooting' in the IT knowledge base"
- "Write an article about VPN setup in the Network Troubleshooting category"
- "Update the VPN setup article to include mobile device instructions"
- "Publish the VPN setup article so it's visible to all users"
- "List all articles in the Network Troubleshooting category"
- "Show me the details of the VPN setup article"
- "Find knowledge articles containing 'password reset' in the IT knowledge base"
- "Create a subcategory called 'Wireless Networks' under the Network Troubleshooting category"
#### User Management Examples
- "Create a new user Dr. Alice Radiology in the Radiology department"
- "Update Bob's user record to make him the manager of Alice"
- "Assign the ITIL role to Bob so he can approve change requests"
- "List all users in the Radiology department"
- "Create a new group called 'Biomedical Engineering' for managing medical devices"
- "Add an admin user to the Biomedical Engineering group as a member"
- "Update the Biomedical Engineering group to change its manager"
- "Remove a user from the Biomedical Engineering group"
- "Find all active users in the system with 'doctor' in their title"
- "Create a user that will act as an approver for the Radiology department"
- "List all IT support groups in the system"
#### UI Policy Examples
- "Create a UI policy for the 'Software Request' item (sys_id: abc...) named 'Show Justification' that applies when 'software_cost' is greater than 100."
- "For the UI policy 'Show Justification' (sys_id: def...), add an action to make the 'business_justification' variable visible and mandatory."
- "Create another action for policy 'Show Justification' to hide the 'alternative_software' variable."
### Example Scripts
The repository includes example scripts that demonstrate how to use the tools:
- **examples/catalog_optimization_example.py**: Demonstrates how to analyze and improve the ServiceNow Service Catalog
- **examples/change_management_demo.py**: Shows how to create and manage change requests in ServiceNow
## Authentication Methods
### Basic Authentication
```
SERVICENOW_AUTH_TYPE=basic
SERVICENOW_USERNAME=your-username
SERVICENOW_PASSWORD=your-password
```
### OAuth Authentication
```
SERVICENOW_AUTH_TYPE=oauth
SERVICENOW_CLIENT_ID=your-client-id
SERVICENOW_CLIENT_SECRET=your-client-secret
SERVICENOW_TOKEN_URL=https://your-instance.service-now.com/oauth_token.do
```
### API Key Authentication
```
SERVICENOW_AUTH_TYPE=api_key
SERVICENOW_API_KEY=your-api-key
```
## Development
### Documentation
Additional documentation is available in the `docs` directory:
- [Catalog Integration](docs/catalog.md) - Detailed information about the Service Catalog integration
- [Catalog Optimization](docs/catalog_optimization_plan.md) - Detailed plan for catalog optimization features
- [Change Management](docs/change_management.md) - Detailed information about the Change Management tools
- [Workflow Management](docs/workflow_management.md) - Detailed information about the Workflow Management tools
- [Changeset Management](docs/changeset_management.md) - Detailed information about the Changeset Management tools
### Troubleshooting
#### Common Errors with Change Management Tools
1. **Error: `argument after ** must be a mapping, not CreateChangeRequestParams`**
- This error occurs when you pass a `CreateChangeRequestParams` object instead of a dictionary to the `create_change_request` function.
- Solution: Make sure you're passing a dictionary with the parameters, not a Pydantic model object.
- 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.
2. **Error: `Missing required parameter 'type'`**
- This error occurs when you don't provide all required parameters for creating a change request.
- Solution: Make sure to include all required parameters. For `create_change_request`, both `short_description` and `type` are required.
3. **Error: `Invalid value for parameter 'type'`**
- This error occurs when you provide an invalid value for the `type` parameter.
- Solution: Use one of the valid values: "normal", "standard", or "emergency".
4. **Error: `Cannot find get_headers method in either auth_manager or server_config`**
- This error occurs when the parameters are passed in the wrong order or when using objects that don't have the required methods.
- 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.
### Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
### License
This project is licensed under the MIT License - see the LICENSE file for details.
```
--------------------------------------------------------------------------------
/src/servicenow_mcp/auth/__init__.py:
--------------------------------------------------------------------------------
```python
"""
Authentication module for the ServiceNow MCP server.
"""
from servicenow_mcp.auth.auth_manager import AuthManager
__all__ = ["AuthManager"]
```
--------------------------------------------------------------------------------
/src/servicenow_mcp/__init__.py:
--------------------------------------------------------------------------------
```python
"""
ServiceNow MCP Server
A Model Context Protocol (MCP) server implementation for ServiceNow,
focusing on the ITSM module.
"""
__version__ = "0.1.0"
from servicenow_mcp.server import ServiceNowMCP
__all__ = ["ServiceNowMCP"]
```
--------------------------------------------------------------------------------
/src/servicenow_mcp/utils/__init__.py:
--------------------------------------------------------------------------------
```python
"""
Utilities module for the ServiceNow MCP server.
"""
from servicenow_mcp.utils.config import (
ApiKeyConfig,
AuthConfig,
AuthType,
BasicAuthConfig,
OAuthConfig,
ServerConfig,
)
__all__ = [
"ApiKeyConfig",
"AuthConfig",
"AuthType",
"BasicAuthConfig",
"OAuthConfig",
"ServerConfig",
]
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# Copy project files
COPY pyproject.toml README.md LICENSE ./
COPY src/ ./src/
# Install the package in development mode
RUN pip install -e .
# Expose the port the app runs on
EXPOSE 8080
# Command to run the application using the provided CLI
CMD ["servicenow-mcp-sse", "--host=0.0.0.0", "--port=8080"]
```
--------------------------------------------------------------------------------
/examples/claude_desktop_config.json:
--------------------------------------------------------------------------------
```json
{
"mcpServers": {
"servicenow": {
"command": "python",
"args": [
"-m",
"servicenow_mcp.cli"
],
"env": {
"SERVICENOW_INSTANCE_URL": "https://your-instance.service-now.com",
"SERVICENOW_USERNAME": "your-username",
"SERVICENOW_PASSWORD": "your-password",
"SERVICENOW_AUTH_TYPE": "basic"
}
}
}
}
```
--------------------------------------------------------------------------------
/src/servicenow_mcp/utils/config.py:
--------------------------------------------------------------------------------
```python
"""
Configuration module for the ServiceNow MCP server.
"""
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
class AuthType(str, Enum):
"""Authentication types supported by the ServiceNow MCP server."""
BASIC = "basic"
OAUTH = "oauth"
API_KEY = "api_key"
class BasicAuthConfig(BaseModel):
"""Configuration for basic authentication."""
username: str
password: str
class OAuthConfig(BaseModel):
"""Configuration for OAuth authentication."""
client_id: str
client_secret: str
username: str
password: str
token_url: Optional[str] = None
class ApiKeyConfig(BaseModel):
"""Configuration for API key authentication."""
api_key: str
header_name: str = "X-ServiceNow-API-Key"
class AuthConfig(BaseModel):
"""Authentication configuration."""
type: AuthType
basic: Optional[BasicAuthConfig] = None
oauth: Optional[OAuthConfig] = None
api_key: Optional[ApiKeyConfig] = None
class ServerConfig(BaseModel):
"""Server configuration."""
instance_url: str
auth: AuthConfig
debug: bool = False
timeout: int = 30
@property
def api_url(self) -> str:
"""Get the API URL for the ServiceNow instance."""
return f"{self.instance_url}/api/now"
```
--------------------------------------------------------------------------------
/scripts/install_claude_desktop.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Check if Claude Desktop config file exists
CONFIG_FILE="$HOME/.config/claude/claude_desktop_config.json"
BACKUP_FILE="$HOME/.config/claude/claude_desktop_config.json.bak"
# Create config directory if it doesn't exist
mkdir -p "$HOME/.config/claude"
# Backup existing config if it exists
if [ -f "$CONFIG_FILE" ]; then
echo "Backing up existing Claude Desktop configuration..."
cp "$CONFIG_FILE" "$BACKUP_FILE"
fi
# Get the absolute path to the current directory
CURRENT_DIR=$(pwd)
# Create or update the Claude Desktop config
echo "Creating Claude Desktop configuration..."
cat > "$CONFIG_FILE" << EOL
{
"mcpServers": {
"servicenow": {
"command": "$CURRENT_DIR/.venv/bin/python",
"args": [
"-m",
"servicenow_mcp.cli"
],
"env": {
"SERVICENOW_INSTANCE_URL": "$(grep SERVICENOW_INSTANCE_URL .env | cut -d '=' -f2)",
"SERVICENOW_USERNAME": "$(grep SERVICENOW_USERNAME .env | cut -d '=' -f2)",
"SERVICENOW_PASSWORD": "$(grep SERVICENOW_PASSWORD .env | cut -d '=' -f2)",
"SERVICENOW_AUTH_TYPE": "$(grep SERVICENOW_AUTH_TYPE .env | head -1 | cut -d '=' -f2)"
}
}
}
}
EOL
echo "ServiceNow MCP server installed in Claude Desktop!"
echo "You can now use it by opening Claude Desktop and selecting the ServiceNow MCP server."
echo ""
echo "If you need to update your ServiceNow credentials, edit the .env file and run this script again."
```
--------------------------------------------------------------------------------
/scripts/setup.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Create directory if it doesn't exist
mkdir -p scripts
# Check if uv is installed
if ! command -v uv &> /dev/null; then
echo "uv is not installed. Installing..."
pip install uv
fi
# Create virtual environment
echo "Creating virtual environment..."
uv venv .venv
# Activate virtual environment
echo "Activating virtual environment..."
source .venv/bin/activate
# Install dependencies
echo "Installing dependencies..."
uv pip install -e ".[dev]"
# Create .env file if it doesn't exist
if [ ! -f .env ]; then
echo "Creating .env file..."
cat > .env << EOL
# ServiceNow Instance Configuration
SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com
SERVICENOW_USERNAME=your-username
SERVICENOW_PASSWORD=your-password
# OAuth Configuration (if using OAuth)
SERVICENOW_AUTH_TYPE=basic
# SERVICENOW_AUTH_TYPE=oauth
# SERVICENOW_CLIENT_ID=your-client-id
# SERVICENOW_CLIENT_SECRET=your-client-secret
# SERVICENOW_TOKEN_URL=https://your-instance.service-now.com/oauth_token.do
# API Key Configuration (if using API Key)
# SERVICENOW_AUTH_TYPE=api_key
# SERVICENOW_API_KEY=your-api-key
# SERVICENOW_API_KEY_HEADER=X-ServiceNow-API-Key
# Debug Configuration
SERVICENOW_DEBUG=false
SERVICENOW_TIMEOUT=30
EOL
echo "Please update the .env file with your ServiceNow credentials."
fi
echo "Setup complete! You can now run the server with:"
echo "python examples/basic_server.py"
echo ""
echo "To use with Claude Desktop, copy the configuration from examples/claude_desktop_config.json to your Claude Desktop configuration."
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "servicenow-mcp"
version = "0.1.0"
description = "A Model Context Protocol (MCP) server implementation for ServiceNow"
readme = "README.md"
requires-python = ">=3.11"
license = {file = "LICENSE"}
authors = [
{name = "ServiceNow MCP Contributors"},
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
]
dependencies = [
"mcp[cli]==1.3.0",
"requests>=2.28.0",
"pydantic>=2.0.0",
"python-dotenv>=1.0.0",
"starlette>=0.27.0",
"uvicorn>=0.22.0",
"httpx>=0.24.0",
"PyYAML>=6.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"isort>=5.12.0",
"mypy>=1.0.0",
"ruff>=0.0.1",
]
[project.scripts]
servicenow-mcp = "servicenow_mcp.cli:main"
servicenow-mcp-sse = "servicenow_mcp.server_sse:main"
[tool.hatch.build.targets.wheel]
packages = ["src/servicenow_mcp"]
[tool.black]
line-length = 100
target-version = ["py311"]
[tool.isort]
profile = "black"
line_length = 100
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
[tool.ruff]
line-length = 100
target-version = "py311"
select = ["E", "F", "B", "I"]
ignore = []
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = "--ignore=examples"
```
--------------------------------------------------------------------------------
/tests/test_server_catalog_optimization.py:
--------------------------------------------------------------------------------
```python
"""
Tests for the ServiceNow MCP server integration with catalog optimization tools.
"""
import unittest
from unittest.mock import MagicMock, patch
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.tools.catalog_optimization import (
OptimizationRecommendationsParams,
UpdateCatalogItemParams,
get_optimization_recommendations,
update_catalog_item,
)
from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
class TestCatalogOptimizationToolParameters(unittest.TestCase):
"""Test cases for the catalog optimization tool parameters."""
def test_tool_parameter_classes(self):
"""Test that the parameter classes for the tools are properly defined."""
# Test OptimizationRecommendationsParams
params = OptimizationRecommendationsParams(
recommendation_types=["inactive_items", "low_usage"],
category_id="hardware"
)
self.assertEqual(params.recommendation_types, ["inactive_items", "low_usage"])
self.assertEqual(params.category_id, "hardware")
# Test with default values
params = OptimizationRecommendationsParams(
recommendation_types=["inactive_items"]
)
self.assertEqual(params.recommendation_types, ["inactive_items"])
self.assertIsNone(params.category_id)
# Test UpdateCatalogItemParams
params = UpdateCatalogItemParams(
item_id="item1",
name="Updated Laptop",
short_description="High-performance laptop",
description="Detailed description of the laptop",
category="hardware",
price="1099.99",
active=True,
order=100
)
self.assertEqual(params.item_id, "item1")
self.assertEqual(params.name, "Updated Laptop")
self.assertEqual(params.short_description, "High-performance laptop")
self.assertEqual(params.description, "Detailed description of the laptop")
self.assertEqual(params.category, "hardware")
self.assertEqual(params.price, "1099.99")
self.assertTrue(params.active)
self.assertEqual(params.order, 100)
# Test with only required parameters
params = UpdateCatalogItemParams(
item_id="item1"
)
self.assertEqual(params.item_id, "item1")
self.assertIsNone(params.name)
self.assertIsNone(params.short_description)
self.assertIsNone(params.description)
self.assertIsNone(params.category)
self.assertIsNone(params.price)
self.assertIsNone(params.active)
self.assertIsNone(params.order)
if __name__ == "__main__":
unittest.main()
```
--------------------------------------------------------------------------------
/tests/test_server_workflow.py:
--------------------------------------------------------------------------------
```python
"""
Tests for the ServiceNow MCP server workflow management integration.
"""
import unittest
from unittest.mock import MagicMock, patch
from servicenow_mcp.server import ServiceNowMCP
from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
class TestServerWorkflow(unittest.TestCase):
"""Tests for the ServiceNow MCP server workflow management integration."""
def setUp(self):
"""Set up test fixtures."""
self.auth_config = AuthConfig(
type=AuthType.BASIC,
basic=BasicAuthConfig(username="test_user", password="test_password"),
)
self.server_config = ServerConfig(
instance_url="https://test.service-now.com",
auth=self.auth_config,
)
# Create a mock FastMCP instance
self.mock_mcp = MagicMock()
# Patch the FastMCP class
self.patcher = patch("servicenow_mcp.server.FastMCP", return_value=self.mock_mcp)
self.mock_fastmcp = self.patcher.start()
# Create the server instance
self.server = ServiceNowMCP(self.server_config)
def tearDown(self):
"""Tear down test fixtures."""
self.patcher.stop()
def test_register_workflow_tools(self):
"""Test that workflow tools are registered with the MCP server."""
# Get all the tool decorator calls
tool_decorator_calls = self.mock_mcp.tool.call_count
# Verify that the tool decorator was called at least 12 times (for all workflow tools)
self.assertGreaterEqual(tool_decorator_calls, 12,
"Expected at least 12 tool registrations for workflow tools")
# Check that the workflow tools are registered by examining the decorated functions
decorated_functions = []
for call in self.mock_mcp.tool.call_args_list:
# Each call to tool() returns a decorator function
decorator = call[0][0] if call[0] else call[1].get('return_value', None)
if decorator:
decorated_functions.append(decorator.__name__)
# Check for workflow tool registrations
workflow_tools = [
"list_workflows",
"get_workflow_details",
"list_workflow_versions",
"get_workflow_activities",
"create_workflow",
"update_workflow",
"activate_workflow",
"deactivate_workflow",
"add_workflow_activity",
"update_workflow_activity",
"delete_workflow_activity",
"reorder_workflow_activities",
]
# Print the decorated functions for debugging
print(f"Decorated functions: {decorated_functions}")
# Check that all workflow tools are registered
for tool in workflow_tools:
self.assertIn(tool, str(self.mock_mcp.mock_calls),
f"Expected {tool} to be registered")
if __name__ == "__main__":
unittest.main()
```
--------------------------------------------------------------------------------
/examples/claude_incident_demo.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
Claude Desktop Incident Management Demo
This script demonstrates how to use the ServiceNow MCP server with Claude Desktop
to manage incidents.
Prerequisites:
1. Claude Desktop installed
2. ServiceNow MCP server configured in Claude Desktop
3. Valid ServiceNow credentials
"""
import json
import os
import subprocess
import sys
from pathlib import Path
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Get configuration from environment variables
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
username = os.getenv("SERVICENOW_USERNAME")
password = os.getenv("SERVICENOW_PASSWORD")
if not instance_url or not username or not password:
print("Error: Missing required environment variables.")
print("Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
sys.exit(1)
# Create Claude Desktop configuration
claude_config = {
"mcpServers": {
"servicenow": {
"command": "python",
"args": [
"-m",
"servicenow_mcp.cli"
],
"env": {
"SERVICENOW_INSTANCE_URL": instance_url,
"SERVICENOW_USERNAME": username,
"SERVICENOW_PASSWORD": password,
"SERVICENOW_AUTH_TYPE": "basic"
}
}
}
}
# Save configuration to a temporary file
config_path = Path.home() / ".claude-desktop" / "config.json"
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w") as f:
json.dump(claude_config, f, indent=2)
print(f"Claude Desktop configuration saved to {config_path}")
print("You can now start Claude Desktop and use the following prompts:")
print("\n=== Example Prompts ===")
print("\n1. List recent incidents:")
print(" Can you list the 5 most recent incidents in ServiceNow?")
print("\n2. Get incident details:")
print(" Can you show me the details of incident INC0010001?")
print("\n3. Create a new incident:")
print(" Please create a new incident in ServiceNow with the following details:")
print(" - Short description: Email service is down")
print(" - Description: Users are unable to send or receive emails")
print(" - Category: Software")
print(" - Priority: 1")
print("\n4. Update an incident:")
print(" Please update incident INC0010001 with the following changes:")
print(" - Priority: 2")
print(" - Assigned to: admin")
print(" - Add work note: Investigating the issue")
print("\n5. Resolve an incident:")
print(" Please resolve incident INC0010001 with the following details:")
print(" - Resolution code: Solved (Permanently)")
print(" - Resolution notes: The email service has been restored")
print("\n=== Starting Claude Desktop ===")
print("Press Ctrl+C to exit this script and continue using Claude Desktop.")
try:
# Try to start Claude Desktop
subprocess.run(["claude"], check=True)
except KeyboardInterrupt:
print("\nExiting script. Claude Desktop should be running.")
except Exception as e:
print(f"\nFailed to start Claude Desktop: {e}")
print("Please start Claude Desktop manually.")
```
--------------------------------------------------------------------------------
/tests/test_incident_tools.py:
--------------------------------------------------------------------------------
```python
import unittest
from unittest.mock import MagicMock, patch
from servicenow_mcp.tools.incident_tools import get_incident_by_number, GetIncidentByNumberParams
from servicenow_mcp.utils.config import ServerConfig, AuthConfig, AuthType, BasicAuthConfig
from servicenow_mcp.auth.auth_manager import AuthManager
class TestIncidentTools(unittest.TestCase):
def setUp(self):
self.auth_config = AuthConfig(type=AuthType.BASIC, basic=BasicAuthConfig(username='test', password='test'))
@patch('requests.get')
def test_get_incident_by_number_success(self, mock_get):
# Mock the server configuration
config = ServerConfig(instance_url="https://dev12345.service-now.com", auth=self.auth_config)
# Mock the authentication manager
auth_manager = MagicMock(spec=AuthManager)
auth_manager.get_headers.return_value = {"Authorization": "Bearer FAKE_TOKEN"}
# Mock the requests.get call
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"result": [
{
"sys_id": "12345",
"number": "INC0010001",
"short_description": "Test incident",
"description": "This is a test incident",
"state": "New",
"priority": "1 - Critical",
"assigned_to": "John Doe",
"category": "Software",
"subcategory": "Email",
"sys_created_on": "2025-06-25 10:00:00",
"sys_updated_on": "2025-06-25 10:00:00"
}
]
}
mock_get.return_value = mock_response
# Call the function with test data
params = GetIncidentByNumberParams(incident_number="INC0010001")
result = get_incident_by_number(config, auth_manager, params)
# Assert the results
self.assertTrue(result["success"])
self.assertEqual(result["message"], "Incident INC0010001 found")
self.assertIn("incident", result)
self.assertEqual(result["incident"]["number"], "INC0010001")
@patch('requests.get')
def test_get_incident_by_number_not_found(self, mock_get):
# Mock the server configuration
config = ServerConfig(instance_url="https://dev12345.service-now.com", auth=self.auth_config)
# Mock the authentication manager
auth_manager = MagicMock(spec=AuthManager)
auth_manager.get_headers.return_value = {"Authorization": "Bearer FAKE_TOKEN"}
# Mock the requests.get call for a not found scenario
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"result": []}
mock_get.return_value = mock_response
# Call the function with a non-existent incident number
params = GetIncidentByNumberParams(incident_number="INC9999999")
result = get_incident_by_number(config, auth_manager, params)
# Assert the results
self.assertFalse(result["success"])
self.assertEqual(result["message"], "Incident not found: INC9999999")
if __name__ == '__main__':
unittest.main()
```
--------------------------------------------------------------------------------
/examples/claude_catalog_demo.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
Claude Desktop Service Catalog Demo
This script demonstrates how to use the ServiceNow MCP server with Claude Desktop
to interact with the ServiceNow Service Catalog.
Prerequisites:
1. Claude Desktop installed
2. ServiceNow MCP server configured in Claude Desktop
3. Valid ServiceNow credentials with access to the Service Catalog
Usage:
python examples/claude_catalog_demo.py [--dry-run]
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from dotenv import load_dotenv
# Parse command line arguments
parser = argparse.ArgumentParser(description="Claude Desktop Service Catalog Demo")
parser.add_argument("--dry-run", action="store_true", help="Skip launching Claude Desktop")
args = parser.parse_args()
# Load environment variables
load_dotenv()
# Get configuration from environment variables
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
username = os.getenv("SERVICENOW_USERNAME")
password = os.getenv("SERVICENOW_PASSWORD")
if not instance_url or not username or not password:
print("Error: Missing required environment variables.")
print("Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
sys.exit(1)
# Create Claude Desktop configuration
claude_config = {
"mcpServers": {
"servicenow": {
"command": "python",
"args": ["-m", "servicenow_mcp.cli"],
"env": {
"SERVICENOW_INSTANCE_URL": instance_url,
"SERVICENOW_USERNAME": username,
"SERVICENOW_PASSWORD": password,
"SERVICENOW_AUTH_TYPE": "basic",
},
}
}
}
# Save configuration to a temporary file
config_path = Path.home() / ".claude-desktop" / "config.json"
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w") as f:
json.dump(claude_config, f, indent=2)
print(f"Claude Desktop configuration saved to {config_path}")
print("You can now start Claude Desktop and use the following prompts:")
print("\n=== Example Prompts for Service Catalog ===")
print("\n1. List catalog categories:")
print(" Can you list the available service catalog categories in ServiceNow?")
print("\n2. List catalog items:")
print(" Can you show me the available items in the ServiceNow service catalog?")
print("\n3. List items in a specific category:")
print(" Can you list the catalog items in the Hardware category?")
print("\n4. Get catalog item details:")
print(" Can you show me the details of the 'New Laptop' catalog item?")
print("\n5. Find items by keyword:")
print(" Can you find catalog items related to 'software' in ServiceNow?")
print("\n6. Compare catalog items:")
print(" Can you compare the different laptop options available in the service catalog?")
print("\n7. Explain catalog item variables:")
print(" What information do I need to provide when ordering a new laptop?")
if args.dry_run:
print("\n=== Dry Run Mode ===")
print("Skipping Claude Desktop launch. Start Claude Desktop manually to use the configuration.")
sys.exit(0)
print("\n=== Starting Claude Desktop ===")
print("Press Ctrl+C to exit this script and continue using Claude Desktop.")
try:
# Try to start Claude Desktop
subprocess.run(["claude"], check=True)
except KeyboardInterrupt:
print("\nExiting script. Claude Desktop should be running.")
except Exception as e:
print(f"\nFailed to start Claude Desktop: {e}")
print("Please start Claude Desktop manually.")
```
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
```python
"""
Tests for the configuration module.
"""
from servicenow_mcp.utils.config import (
ApiKeyConfig,
AuthConfig,
AuthType,
BasicAuthConfig,
OAuthConfig,
ServerConfig,
)
def test_auth_type_enum():
"""Test the AuthType enum."""
assert AuthType.BASIC == "basic"
assert AuthType.OAUTH == "oauth"
assert AuthType.API_KEY == "api_key"
def test_basic_auth_config():
"""Test the BasicAuthConfig class."""
config = BasicAuthConfig(username="user", password="pass")
assert config.username == "user"
assert config.password == "pass"
def test_oauth_config():
"""Test the OAuthConfig class."""
config = OAuthConfig(
client_id="client_id",
client_secret="client_secret",
username="user",
password="pass",
)
assert config.client_id == "client_id"
assert config.client_secret == "client_secret"
assert config.username == "user"
assert config.password == "pass"
assert config.token_url is None
config = OAuthConfig(
client_id="client_id",
client_secret="client_secret",
username="user",
password="pass",
token_url="https://example.com/token",
)
assert config.token_url == "https://example.com/token"
def test_api_key_config():
"""Test the ApiKeyConfig class."""
config = ApiKeyConfig(api_key="api_key")
assert config.api_key == "api_key"
assert config.header_name == "X-ServiceNow-API-Key"
config = ApiKeyConfig(api_key="api_key", header_name="Custom-Header")
assert config.header_name == "Custom-Header"
def test_auth_config():
"""Test the AuthConfig class."""
# Basic auth
config = AuthConfig(
type=AuthType.BASIC,
basic=BasicAuthConfig(username="user", password="pass"),
)
assert config.type == AuthType.BASIC
assert config.basic is not None
assert config.basic.username == "user"
assert config.basic.password == "pass"
assert config.oauth is None
assert config.api_key is None
# OAuth
config = AuthConfig(
type=AuthType.OAUTH,
oauth=OAuthConfig(
client_id="client_id",
client_secret="client_secret",
username="user",
password="pass",
),
)
assert config.type == AuthType.OAUTH
assert config.oauth is not None
assert config.oauth.client_id == "client_id"
assert config.basic is None
assert config.api_key is None
# API key
config = AuthConfig(
type=AuthType.API_KEY,
api_key=ApiKeyConfig(api_key="api_key"),
)
assert config.type == AuthType.API_KEY
assert config.api_key is not None
assert config.api_key.api_key == "api_key"
assert config.basic is None
assert config.oauth is None
def test_server_config():
"""Test the ServerConfig class."""
config = ServerConfig(
instance_url="https://example.service-now.com",
auth=AuthConfig(
type=AuthType.BASIC,
basic=BasicAuthConfig(username="user", password="pass"),
),
)
assert config.instance_url == "https://example.service-now.com"
assert config.auth.type == AuthType.BASIC
assert config.debug is False
assert config.timeout == 30
assert config.api_url == "https://example.service-now.com/api/now"
config = ServerConfig(
instance_url="https://example.service-now.com",
auth=AuthConfig(
type=AuthType.BASIC,
basic=BasicAuthConfig(username="user", password="pass"),
),
debug=True,
timeout=60,
)
assert config.debug is True
assert config.timeout == 60
```
--------------------------------------------------------------------------------
/scripts/setup_api_key.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
ServiceNow API Key Setup Script
This script helps set up and test API key authentication with ServiceNow.
It will:
1. Test the API key with a simple API call
2. Update the .env file with the API key configuration
Usage:
python scripts/setup_api_key.py
"""
import os
import sys
import requests
from pathlib import Path
from dotenv import load_dotenv
# Add the project root to the Python path
sys.path.insert(0, str(Path(__file__).parent.parent))
def setup_api_key():
# Load environment variables
load_dotenv()
# Get ServiceNow instance URL
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
if not instance_url or instance_url == "https://your-instance.service-now.com":
instance_url = input("Enter your ServiceNow instance URL (e.g., https://dev296866.service-now.com): ")
print("\n=== ServiceNow API Key Setup ===")
print("This script will help you set up API key authentication for your ServiceNow instance.")
print("You'll need to create an API key in ServiceNow first.")
print("\nTo create an API key in a Personal Developer Instance (PDI):")
print("1. Log in to your ServiceNow instance")
print("2. Navigate to User Profile > REST API Keys")
print("3. Click 'New'")
print("4. Fill in the required fields and save")
print("5. Copy the API key (you'll only see it once)")
# Get API key
api_key = input("\nEnter your API key: ")
api_key_header = input("Enter the API key header name (default: X-ServiceNow-API-Key): ") or "X-ServiceNow-API-Key"
print(f"\nTesting API key connection to {instance_url}...")
# Test the API key
try:
# Make a test request
test_url = f"{instance_url}/api/now/table/incident?sysparm_limit=1"
test_response = requests.get(
test_url,
headers={
api_key_header: api_key,
'Accept': 'application/json'
}
)
if test_response.status_code == 200:
print("✅ Successfully tested API key with API call!")
data = test_response.json()
print(f"Retrieved {len(data.get('result', []))} incident(s)")
# Update .env file
update_env = input("\nDo you want to update your .env file with this API key? (y/n): ")
if update_env.lower() == 'y':
env_path = Path(__file__).parent.parent / '.env'
with open(env_path, 'r') as f:
env_content = f.read()
# Update API key configuration
env_content = env_content.replace('SERVICENOW_AUTH_TYPE=basic', 'SERVICENOW_AUTH_TYPE=api_key')
env_content = env_content.replace('# SERVICENOW_API_KEY=your-api-key', f'SERVICENOW_API_KEY={api_key}')
env_content = env_content.replace('# SERVICENOW_API_KEY_HEADER=X-ServiceNow-API-Key', f'SERVICENOW_API_KEY_HEADER={api_key_header}')
with open(env_path, 'w') as f:
f.write(env_content)
print("✅ Updated .env file with API key configuration!")
print("\nYou can now use API key authentication with the ServiceNow MCP server.")
print("To test it, run: python scripts/test_connection.py")
return True
else:
print(f"❌ Failed to test API key with API call: {test_response.status_code}")
print(f"Response: {test_response.text}")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Connection error: {e}")
return False
if __name__ == "__main__":
setup_api_key()
```
--------------------------------------------------------------------------------
/scripts/setup_auth.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
ServiceNow Authentication Setup Menu
This script provides a menu to help users set up different authentication methods
for the ServiceNow MCP server.
Usage:
python scripts/setup_auth.py
"""
import os
import sys
import subprocess
from pathlib import Path
def clear_screen():
"""Clear the terminal screen."""
os.system('cls' if os.name == 'nt' else 'clear')
def print_header():
"""Print the header for the menu."""
print("=" * 60)
print("ServiceNow MCP Server - Authentication Setup".center(60))
print("=" * 60)
print("\nThis script will help you set up authentication for your ServiceNow instance.")
print("Choose one of the following authentication methods:\n")
def print_menu():
"""Print the menu options."""
print("1. Basic Authentication (username/password)")
print("2. OAuth Authentication (client ID/client secret)")
print("3. API Key Authentication")
print("4. Test Current Configuration")
print("5. Exit")
print("\nEnter your choice (1-5): ", end="")
def setup_basic_auth():
"""Set up basic authentication."""
clear_screen()
print("=" * 60)
print("Basic Authentication Setup".center(60))
print("=" * 60)
print("\nYou'll need your ServiceNow instance URL, username, and password.")
instance_url = input("\nEnter your ServiceNow instance URL: ")
username = input("Enter your ServiceNow username: ")
password = input("Enter your ServiceNow password: ")
# Update .env file
env_path = Path(__file__).parent.parent / '.env'
with open(env_path, 'r') as f:
env_content = f.read()
# Update basic authentication configuration
env_content = env_content.replace('SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com', f'SERVICENOW_INSTANCE_URL={instance_url}')
env_content = env_content.replace('SERVICENOW_USERNAME=your-username', f'SERVICENOW_USERNAME={username}')
env_content = env_content.replace('SERVICENOW_PASSWORD=your-password', f'SERVICENOW_PASSWORD={password}')
# Ensure auth type is set to basic
if 'SERVICENOW_AUTH_TYPE=oauth' in env_content:
env_content = env_content.replace('SERVICENOW_AUTH_TYPE=oauth', 'SERVICENOW_AUTH_TYPE=basic')
elif 'SERVICENOW_AUTH_TYPE=api_key' in env_content:
env_content = env_content.replace('SERVICENOW_AUTH_TYPE=api_key', 'SERVICENOW_AUTH_TYPE=basic')
with open(env_path, 'w') as f:
f.write(env_content)
print("\n✅ Updated .env file with basic authentication configuration!")
input("\nPress Enter to continue...")
def main():
"""Main function to run the menu."""
while True:
clear_screen()
print_header()
print_menu()
choice = input()
if choice == '1':
setup_basic_auth()
elif choice == '2':
# Run the OAuth setup script
subprocess.run([sys.executable, str(Path(__file__).parent / 'setup_oauth.py')])
input("\nPress Enter to continue...")
elif choice == '3':
# Run the API key setup script
subprocess.run([sys.executable, str(Path(__file__).parent / 'setup_api_key.py')])
input("\nPress Enter to continue...")
elif choice == '4':
# Run the test connection script
clear_screen()
print("Testing current configuration...\n")
subprocess.run([sys.executable, str(Path(__file__).parent / 'test_connection.py')])
input("\nPress Enter to continue...")
elif choice == '5':
clear_screen()
print("Exiting...")
break
else:
print("Invalid choice. Please try again.")
input("\nPress Enter to continue...")
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/examples/wake_servicenow_instance.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
Wake ServiceNow Instance
This script attempts to wake up a hibernating ServiceNow instance by
making requests to it and following any redirects to the wake-up page.
"""
import os
import sys
import time
import logging
import requests
from dotenv import load_dotenv
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def wake_instance(instance_url, max_attempts=5, wait_time=10):
"""
Attempt to wake up a hibernating ServiceNow instance.
Args:
instance_url: The URL of the ServiceNow instance
max_attempts: Maximum number of wake-up attempts
wait_time: Time to wait between attempts (seconds)
Returns:
bool: True if the instance appears to be awake, False otherwise
"""
logger.info(f"Attempting to wake up ServiceNow instance: {instance_url}")
# Create a session to handle cookies and redirects
session = requests.Session()
for attempt in range(1, max_attempts + 1):
logger.info(f"Wake-up attempt {attempt}/{max_attempts}...")
try:
# Make a request to the instance
response = session.get(
instance_url,
allow_redirects=True,
timeout=30
)
# Check if we got a JSON response from the API
if "application/json" in response.headers.get("Content-Type", ""):
logger.info("Instance appears to be awake (JSON response received)")
return True
# Check if we're still getting the hibernation page
if "Instance Hibernating" in response.text:
logger.info("Instance is still hibernating")
# Look for the wake-up URL in the page
if "wu=true" in response.text:
wake_url = "https://developer.servicenow.com/dev.do#!/home?wu=true"
logger.info(f"Following wake-up URL: {wake_url}")
# Make a request to the wake-up URL
wake_response = session.get(wake_url, allow_redirects=True, timeout=30)
logger.info(f"Wake-up request status: {wake_response.status_code}")
else:
# Check if we got a login page or something else
logger.info(f"Got response with status {response.status_code}, but not the hibernation page")
logger.info(f"Content type: {response.headers.get('Content-Type')}")
# Wait before the next attempt
if attempt < max_attempts:
logger.info(f"Waiting {wait_time} seconds before next attempt...")
time.sleep(wait_time)
except requests.RequestException as e:
logger.error(f"Error during wake-up attempt: {e}")
if attempt < max_attempts:
logger.info(f"Waiting {wait_time} seconds before next attempt...")
time.sleep(wait_time)
logger.warning(f"Failed to wake up instance after {max_attempts} attempts")
return False
def main():
"""Main function."""
# Load environment variables
load_dotenv()
# Get ServiceNow instance URL
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
if not instance_url:
logger.error("SERVICENOW_INSTANCE_URL environment variable is not set")
sys.exit(1)
# Try to wake up the instance
success = wake_instance(instance_url)
if success:
logger.info("ServiceNow instance wake-up process completed successfully")
sys.exit(0)
else:
logger.error("Failed to wake up ServiceNow instance")
logger.info("You may need to manually wake up the instance by visiting:")
logger.info("https://developer.servicenow.com/dev.do#!/home?wu=true")
sys.exit(1)
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/scripts/check_pdi_status.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
ServiceNow PDI Status Checker
This script checks the status of a ServiceNow PDI instance.
It will:
1. Check if the instance is reachable
2. Check if the instance is active or hibernating
3. Provide guidance on waking up a hibernating instance
Usage:
python scripts/check_pdi_status.py
"""
import os
import sys
import requests
from pathlib import Path
from dotenv import load_dotenv
# Add the project root to the Python path
sys.path.insert(0, str(Path(__file__).parent.parent))
def check_instance_status(instance_url):
"""Check the status of a ServiceNow instance."""
print(f"\nChecking instance status: {instance_url}")
# Check if the instance is reachable
try:
# Try accessing the login page
login_response = requests.get(f"{instance_url}/login.do",
allow_redirects=True,
timeout=10)
# Try accessing the API
api_response = requests.get(f"{instance_url}/api/now/table/sys_properties?sysparm_limit=1",
headers={"Accept": "application/json"},
timeout=10)
# Check if the instance is hibernating
if "instance is hibernating" in login_response.text.lower() or "instance is hibernating" in api_response.text.lower():
print("❌ Instance is HIBERNATING")
print("\nYour PDI is currently hibernating. To wake it up:")
print("1. Go to https://developer.servicenow.com/")
print("2. Log in with your ServiceNow account")
print("3. Go to 'My Instances'")
print("4. Find your PDI and click 'Wake'")
print("5. Wait a few minutes for the instance to wake up")
return False
# Check if the instance is accessible
if login_response.status_code == 200 and "ServiceNow" in login_response.text:
print("✅ Instance is ACTIVE and accessible")
print("✅ Login page is available")
# Extract the instance name from the login page
if "instance_name" in login_response.text:
start_index = login_response.text.find("instance_name")
end_index = login_response.text.find(";", start_index)
if start_index > 0 and end_index > start_index:
instance_info = login_response.text[start_index:end_index]
print(f"Instance info: {instance_info}")
return True
else:
print(f"❌ Instance returned unexpected status code: {login_response.status_code}")
print("❌ Login page may not be accessible")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Error connecting to instance: {e}")
return False
def main():
"""Main function to run the PDI status checker."""
load_dotenv()
print("=" * 60)
print("ServiceNow PDI Status Checker".center(60))
print("=" * 60)
# Get instance URL
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
if not instance_url or instance_url == "https://your-instance.service-now.com":
instance_url = input("Enter your ServiceNow instance URL: ")
# Check instance status
is_active = check_instance_status(instance_url)
if is_active:
print("\nYour PDI is active. To find your credentials:")
print("1. Go to https://developer.servicenow.com/")
print("2. Log in with your ServiceNow account")
print("3. Go to 'My Instances'")
print("4. Find your PDI and click on it")
print("5. Look for the credentials information")
print("\nDefault PDI credentials are usually:")
print("Username: admin")
print("Password: (check on the developer portal)")
else:
print("\nPlease check your instance status on the ServiceNow Developer Portal.")
print("If your instance is hibernating, you'll need to wake it up before you can connect.")
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/src/servicenow_mcp/server_sse.py:
--------------------------------------------------------------------------------
```python
"""
ServiceNow MCP Server
This module provides the main implementation of the ServiceNow MCP server.
"""
import argparse
import os
from typing import Dict, Union
import uvicorn
from dotenv import load_dotenv
from mcp.server import Server
from mcp.server.fastmcp import FastMCP
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.routing import Mount, Route
from servicenow_mcp.server import ServiceNowMCP
from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette:
"""Create a Starlette application that can serve the provided mcp server with SSE."""
sse = SseServerTransport("/messages/")
async def handle_sse(request: Request) -> None:
async with sse.connect_sse(
request.scope,
request.receive,
request._send, # noqa: SLF001
) as (read_stream, write_stream):
await mcp_server.run(
read_stream,
write_stream,
mcp_server.create_initialization_options(),
)
return Starlette(
debug=debug,
routes=[
Route("/sse", endpoint=handle_sse),
Mount("/messages/", app=sse.handle_post_message),
],
)
class ServiceNowSSEMCP(ServiceNowMCP):
"""
ServiceNow MCP Server implementation.
This class provides a Model Context Protocol (MCP) server for ServiceNow,
allowing LLMs to interact with ServiceNow data and functionality.
"""
def __init__(self, config: Union[Dict, ServerConfig]):
"""
Initialize the ServiceNow MCP server.
Args:
config: Server configuration, either as a dictionary or ServerConfig object.
"""
super().__init__(config)
def start(self, host: str = "0.0.0.0", port: int = 8080):
"""
Start the MCP server with SSE transport using Starlette and Uvicorn.
Args:
host: Host address to bind to
port: Port to listen on
"""
# Create Starlette app with SSE transport
starlette_app = create_starlette_app(self.mcp_server, debug=True)
# Run using uvicorn
uvicorn.run(starlette_app, host=host, port=port)
def create_servicenow_mcp(instance_url: str, username: str, password: str):
"""
Create a ServiceNow MCP server with minimal configuration.
This is a simplified factory function that creates a pre-configured
ServiceNow MCP server with basic authentication.
Args:
instance_url: ServiceNow instance URL
username: ServiceNow username
password: ServiceNow password
Returns:
A configured ServiceNowMCP instance ready to use
Example:
```python
from servicenow_mcp.server import create_servicenow_mcp
# Create an MCP server for ServiceNow
mcp = create_servicenow_mcp(
instance_url="https://instance.service-now.com",
username="admin",
password="password"
)
# Start the server
mcp.start()
```
"""
# Create basic auth config
auth_config = AuthConfig(
type=AuthType.BASIC, basic=BasicAuthConfig(username=username, password=password)
)
# Create server config
config = ServerConfig(instance_url=instance_url, auth=auth_config)
# Create and return server
return ServiceNowSSEMCP(config)
def main():
load_dotenv()
# Parse command line arguments
parser = argparse.ArgumentParser(description="Run ServiceNow MCP SSE-based server")
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
parser.add_argument("--port", type=int, default=8080, help="Port to listen on")
args = parser.parse_args()
server = create_servicenow_mcp(
instance_url=os.getenv("SERVICENOW_INSTANCE_URL"),
username=os.getenv("SERVICENOW_USERNAME"),
password=os.getenv("SERVICENOW_PASSWORD"),
)
server.start(host=args.host, port=args.port)
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/tests/test_server_catalog.py:
--------------------------------------------------------------------------------
```python
"""
Tests for the ServiceNow MCP server integration with catalog functionality.
"""
import unittest
from unittest.mock import MagicMock, patch
from servicenow_mcp.server import ServiceNowMCP
from servicenow_mcp.tools.catalog_tools import (
GetCatalogItemParams,
ListCatalogCategoriesParams,
ListCatalogItemsParams,
)
from servicenow_mcp.tools.catalog_tools import (
get_catalog_item as get_catalog_item_tool,
)
from servicenow_mcp.tools.catalog_tools import (
list_catalog_categories as list_catalog_categories_tool,
)
from servicenow_mcp.tools.catalog_tools import (
list_catalog_items as list_catalog_items_tool,
)
class TestServerCatalog(unittest.TestCase):
"""Test cases for the server integration with catalog functionality."""
def setUp(self):
"""Set up test fixtures."""
# Create a mock config
self.config = {
"instance_url": "https://example.service-now.com",
"auth": {
"type": "basic",
"basic": {
"username": "admin",
"password": "password",
},
},
}
# Create a mock server
self.server = ServiceNowMCP(self.config)
# Mock the FastMCP server
self.server.mcp_server = MagicMock()
self.server.mcp_server.resource = MagicMock()
self.server.mcp_server.tool = MagicMock()
def test_register_catalog_resources(self):
"""Test that catalog resources are registered correctly."""
# Call the method to register resources
self.server._register_resources()
# Check that the resource decorators were called
resource_calls = self.server.mcp_server.resource.call_args_list
resource_paths = [call[0][0] for call in resource_calls]
# Check that catalog resources are registered
self.assertIn("catalog://items", resource_paths)
self.assertIn("catalog://categories", resource_paths)
self.assertIn("catalog://{item_id}", resource_paths)
def test_register_catalog_tools(self):
"""Test that catalog tools are registered correctly."""
# Call the method to register tools
self.server._register_tools()
# Check that the tool decorator was called
self.server.mcp_server.tool.assert_called()
# Get the tool functions
tool_calls = self.server.mcp_server.tool.call_args_list
# Instead of trying to extract names from the call args, just check that the decorator was called
# the right number of times (at least 3 times for the catalog tools)
self.assertGreaterEqual(len(tool_calls), 3)
@patch("servicenow_mcp.tools.catalog_tools.list_catalog_items")
def test_list_catalog_items_tool(self, mock_list_catalog_items):
"""Test the list_catalog_items tool."""
# Mock the tool function
mock_list_catalog_items.return_value = {
"success": True,
"message": "Retrieved 1 catalog items",
"items": [
{
"sys_id": "item1",
"name": "Laptop",
}
],
}
# Register the tools
self.server._register_tools()
# Check that the tool decorator was called
self.server.mcp_server.tool.assert_called()
@patch("servicenow_mcp.tools.catalog_tools.get_catalog_item")
def test_get_catalog_item_tool(self, mock_get_catalog_item):
"""Test the get_catalog_item tool."""
# Mock the tool function
mock_get_catalog_item.return_value = {
"success": True,
"message": "Retrieved catalog item: Laptop",
"data": {
"sys_id": "item1",
"name": "Laptop",
},
}
# Register the tools
self.server._register_tools()
# Check that the tool decorator was called
self.server.mcp_server.tool.assert_called()
@patch("servicenow_mcp.tools.catalog_tools.list_catalog_categories")
def test_list_catalog_categories_tool(self, mock_list_catalog_categories):
"""Test the list_catalog_categories tool."""
# Mock the tool function
mock_list_catalog_categories.return_value = {
"success": True,
"message": "Retrieved 1 catalog categories",
"categories": [
{
"sys_id": "cat1",
"title": "Hardware",
}
],
}
# Register the tools
self.server._register_tools()
# Check that the tool decorator was called
self.server.mcp_server.tool.assert_called()
if __name__ == "__main__":
unittest.main()
```
--------------------------------------------------------------------------------
/examples/debug_workflow_api.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
Debug script for ServiceNow workflow API calls.
This script helps diagnose issues with the ServiceNow API by making direct calls
and printing detailed information about the requests and responses.
"""
import os
import json
import logging
import requests
from dotenv import load_dotenv
# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Load environment variables
load_dotenv()
# ServiceNow instance details
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
username = os.getenv("SERVICENOW_USERNAME")
password = os.getenv("SERVICENOW_PASSWORD")
if not all([instance_url, username, password]):
logger.error("Missing required environment variables. Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
exit(1)
# Basic auth headers
auth = (username, password)
def debug_request(url, params=None, method="GET"):
"""Make a request to ServiceNow and print detailed debug information."""
logger.info(f"Making {method} request to: {url}")
logger.info(f"Parameters: {params}")
try:
if method == "GET":
response = requests.get(url, auth=auth, params=params)
elif method == "POST":
response = requests.post(url, auth=auth, json=params)
else:
logger.error(f"Unsupported method: {method}")
return
logger.info(f"Status code: {response.status_code}")
logger.info(f"Response headers: {response.headers}")
# Try to parse as JSON
try:
json_response = response.json()
logger.info(f"JSON response: {json.dumps(json_response, indent=2)}")
except json.JSONDecodeError:
logger.warning("Response is not valid JSON")
logger.info(f"Raw response content: {response.content}")
return response
except requests.RequestException as e:
logger.error(f"Request failed: {e}")
return None
def test_list_workflows():
"""Test listing workflows."""
logger.info("=== Testing list_workflows ===")
url = f"{instance_url}/api/now/table/wf_workflow"
params = {
"sysparm_limit": 10,
}
return debug_request(url, params)
def test_list_workflows_active():
"""Test listing active workflows."""
logger.info("=== Testing list_workflows with active=true ===")
url = f"{instance_url}/api/now/table/wf_workflow"
params = {
"sysparm_limit": 10,
"sysparm_query": "active=true",
}
return debug_request(url, params)
def test_get_workflow_details(workflow_id):
"""Test getting workflow details."""
logger.info(f"=== Testing get_workflow_details for {workflow_id} ===")
url = f"{instance_url}/api/now/table/wf_workflow/{workflow_id}"
return debug_request(url)
def test_list_tables():
"""Test listing available tables to check API access."""
logger.info("=== Testing list_tables ===")
url = f"{instance_url}/api/now/table/sys_db_object"
params = {
"sysparm_limit": 5,
"sysparm_fields": "name,label",
}
return debug_request(url, params)
def test_get_user_info():
"""Test getting current user info to verify authentication."""
logger.info("=== Testing get_user_info ===")
url = f"{instance_url}/api/now/table/sys_user"
params = {
"sysparm_query": "user_name=" + username,
"sysparm_fields": "user_name,name,email,roles",
}
return debug_request(url, params)
if __name__ == "__main__":
logger.info(f"Testing ServiceNow API at {instance_url}")
# First, verify authentication and basic API access
user_response = test_get_user_info()
if not user_response or user_response.status_code != 200:
logger.error("Authentication failed or user not found. Please check your credentials.")
exit(1)
# Test listing tables to verify API access
tables_response = test_list_tables()
if not tables_response or tables_response.status_code != 200:
logger.error("Failed to list tables. API access may be restricted.")
exit(1)
# Test workflow API calls
list_response = test_list_workflows()
active_response = test_list_workflows_active()
# If we got any workflows, test getting details for the first one
if list_response and list_response.status_code == 200:
try:
workflows = list_response.json().get("result", [])
if workflows:
workflow_id = workflows[0]["sys_id"]
test_get_workflow_details(workflow_id)
else:
logger.warning("No workflows found in the instance.")
except (json.JSONDecodeError, KeyError) as e:
logger.error(f"Error processing workflow list response: {e}")
logger.info("Debug tests completed.")
```
--------------------------------------------------------------------------------
/debug_workflow_api.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
Debug script for ServiceNow workflow API calls.
This script helps diagnose issues with the ServiceNow API by making direct calls
and printing detailed information about the requests and responses.
"""
import json
import logging
import os
import requests
from dotenv import load_dotenv
# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Load environment variables
load_dotenv()
# ServiceNow instance details
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
username = os.getenv("SERVICENOW_USERNAME")
password = os.getenv("SERVICENOW_PASSWORD")
if not all([instance_url, username, password]):
logger.error("Missing required environment variables. Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
exit(1)
# Basic auth headers
auth = (username, password)
def debug_request(url, params=None, method="GET"):
"""Make a request to ServiceNow and print detailed debug information."""
logger.info(f"Making {method} request to: {url}")
logger.info(f"Parameters: {params}")
try:
if method == "GET":
response = requests.get(url, auth=auth, params=params)
elif method == "POST":
response = requests.post(url, auth=auth, json=params)
else:
logger.error(f"Unsupported method: {method}")
return
logger.info(f"Status code: {response.status_code}")
logger.info(f"Response headers: {response.headers}")
# Try to parse as JSON
try:
json_response = response.json()
logger.info(f"JSON response: {json.dumps(json_response, indent=2)}")
except json.JSONDecodeError:
logger.warning("Response is not valid JSON")
logger.info(f"Raw response content: {response.content}")
return response
except requests.RequestException as e:
logger.error(f"Request failed: {e}")
return None
def test_list_workflows():
"""Test listing workflows."""
logger.info("=== Testing list_workflows ===")
url = f"{instance_url}/api/now/table/wf_workflow"
params = {
"sysparm_limit": 10,
}
return debug_request(url, params)
def test_list_workflows_active():
"""Test listing active workflows."""
logger.info("=== Testing list_workflows with active=true ===")
url = f"{instance_url}/api/now/table/wf_workflow"
params = {
"sysparm_limit": 10,
"sysparm_query": "active=true",
}
return debug_request(url, params)
def test_get_workflow_details(workflow_id):
"""Test getting workflow details."""
logger.info(f"=== Testing get_workflow_details for {workflow_id} ===")
url = f"{instance_url}/api/now/table/wf_workflow/{workflow_id}"
return debug_request(url)
def test_list_tables():
"""Test listing available tables to check API access."""
logger.info("=== Testing list_tables ===")
url = f"{instance_url}/api/now/table/sys_db_object"
params = {
"sysparm_limit": 5,
"sysparm_fields": "name,label",
}
return debug_request(url, params)
def test_get_user_info():
"""Test getting current user info to verify authentication."""
logger.info("=== Testing get_user_info ===")
url = f"{instance_url}/api/now/table/sys_user"
params = {
"sysparm_query": "user_name=" + username,
"sysparm_fields": "user_name,name,email,roles",
}
return debug_request(url, params)
if __name__ == "__main__":
logger.info(f"Testing ServiceNow API at {instance_url}")
# First, verify authentication and basic API access
user_response = test_get_user_info()
if not user_response or user_response.status_code != 200:
logger.error("Authentication failed or user not found. Please check your credentials.")
exit(1)
# Test listing tables to verify API access
tables_response = test_list_tables()
if not tables_response or tables_response.status_code != 200:
logger.error("Failed to list tables. API access may be restricted.")
exit(1)
# Test workflow API calls
list_response = test_list_workflows()
active_response = test_list_workflows_active()
# If we got any workflows, test getting details for the first one
if list_response and list_response.status_code == 200:
try:
workflows = list_response.json().get("result", [])
if workflows:
workflow_id = workflows[0]["sys_id"]
test_get_workflow_details(workflow_id)
else:
logger.warning("No workflows found in the instance.")
except (json.JSONDecodeError, KeyError) as e:
logger.error(f"Error processing workflow list response: {e}")
logger.info("Debug tests completed.")
```
--------------------------------------------------------------------------------
/tests/test_workflow_tools_direct.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
Test script for workflow_tools module.
This script directly tests the workflow_tools functions with proper authentication.
"""
import os
import json
import logging
from dotenv import load_dotenv
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.utils.config import ServerConfig, AuthConfig, AuthType, BasicAuthConfig
from servicenow_mcp.tools.workflow_tools import (
list_workflows,
get_workflow_details,
list_workflow_versions,
get_workflow_activities,
)
# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Load environment variables
load_dotenv()
def setup_auth_and_config():
"""Set up authentication and server configuration."""
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
username = os.getenv("SERVICENOW_USERNAME")
password = os.getenv("SERVICENOW_PASSWORD")
if not all([instance_url, username, password]):
logger.error("Missing required environment variables. Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
exit(1)
# Create authentication configuration
auth_config = AuthConfig(
type=AuthType.BASIC,
basic=BasicAuthConfig(username=username, password=password),
)
# Create server configuration
server_config = ServerConfig(
instance_url=instance_url,
auth=auth_config,
)
# Create authentication manager
auth_manager = AuthManager(auth_config)
return auth_manager, server_config
def print_result(name, result):
"""Print the result of a function call."""
logger.info(f"=== Result of {name} ===")
if "error" in result:
logger.error(f"Error: {result['error']}")
else:
logger.info(json.dumps(result, indent=2))
def test_list_workflows(auth_manager, server_config):
"""Test the list_workflows function."""
logger.info("Testing list_workflows...")
# Test with default parameters
result = list_workflows(auth_manager, server_config, {})
print_result("list_workflows (default)", result)
# Test with active=True
result = list_workflows(auth_manager, server_config, {"active": True})
print_result("list_workflows (active=True)", result)
return result
def test_get_workflow_details(auth_manager, server_config, workflow_id):
"""Test the get_workflow_details function."""
logger.info(f"Testing get_workflow_details for workflow {workflow_id}...")
result = get_workflow_details(auth_manager, server_config, {"workflow_id": workflow_id})
print_result("get_workflow_details", result)
return result
def test_list_workflow_versions(auth_manager, server_config, workflow_id):
"""Test the list_workflow_versions function."""
logger.info(f"Testing list_workflow_versions for workflow {workflow_id}...")
result = list_workflow_versions(auth_manager, server_config, {"workflow_id": workflow_id})
print_result("list_workflow_versions", result)
return result
def test_get_workflow_activities(auth_manager, server_config, workflow_id):
"""Test the get_workflow_activities function."""
logger.info(f"Testing get_workflow_activities for workflow {workflow_id}...")
result = get_workflow_activities(auth_manager, server_config, {"workflow_id": workflow_id})
print_result("get_workflow_activities", result)
return result
def test_with_swapped_params(auth_manager, server_config):
"""Test functions with swapped parameters to verify our fix works."""
logger.info("Testing with swapped parameters...")
# Test list_workflows with swapped parameters
result = list_workflows(server_config, auth_manager, {})
print_result("list_workflows (swapped params)", result)
return result
if __name__ == "__main__":
logger.info("Testing workflow_tools module...")
# Set up authentication and server configuration
auth_manager, server_config = setup_auth_and_config()
# Test list_workflows
workflows_result = test_list_workflows(auth_manager, server_config)
# If we got any workflows, test the other functions
if "workflows" in workflows_result and workflows_result["workflows"]:
workflow_id = workflows_result["workflows"][0]["sys_id"]
# Test get_workflow_details
test_get_workflow_details(auth_manager, server_config, workflow_id)
# Test list_workflow_versions
test_list_workflow_versions(auth_manager, server_config, workflow_id)
# Test get_workflow_activities
test_get_workflow_activities(auth_manager, server_config, workflow_id)
else:
logger.warning("No workflows found, skipping detail tests.")
# Test with swapped parameters
test_with_swapped_params(auth_manager, server_config)
logger.info("Tests completed.")
```
--------------------------------------------------------------------------------
/examples/changeset_management_demo.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python3
"""
Changeset Management Demo
This script demonstrates how to use the ServiceNow MCP server to manage changesets.
It shows how to create, update, commit, and publish changesets, as well as how to
add files to changesets and retrieve changeset details.
"""
import json
import os
import sys
from dotenv import load_dotenv
# Add the parent directory to the path so we can import the servicenow_mcp package
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.tools.changeset_tools import (
add_file_to_changeset,
commit_changeset,
create_changeset,
get_changeset_details,
list_changesets,
publish_changeset,
update_changeset,
)
from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
# Load environment variables from .env file
load_dotenv()
# Get ServiceNow credentials from environment variables
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
username = os.getenv("SERVICENOW_USERNAME")
password = os.getenv("SERVICENOW_PASSWORD")
if not all([instance_url, username, password]):
print("Error: Missing required environment variables.")
print("Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
sys.exit(1)
# Create server configuration
auth_config = AuthConfig(
auth_type=AuthType.BASIC,
basic=BasicAuthConfig(
username=username,
password=password,
),
)
server_config = ServerConfig(
instance_url=instance_url,
auth=auth_config,
)
# Create authentication manager
auth_manager = AuthManager(auth_config)
auth_manager.instance_url = instance_url
def print_json(data):
"""Print JSON data in a readable format."""
print(json.dumps(data, indent=2))
def main():
"""Run the changeset management demo."""
print("\n=== Changeset Management Demo ===\n")
# Step 1: List existing changesets
print("Step 1: Listing existing changesets...")
result = list_changesets(auth_manager, server_config, {
"limit": 5,
"timeframe": "recent",
})
print_json(result)
print("\n")
# Step 2: Create a new changeset
print("Step 2: Creating a new changeset...")
create_result = create_changeset(auth_manager, server_config, {
"name": "Demo Changeset",
"description": "A demonstration changeset created by the MCP demo script",
"application": "Global", # Use a valid application name for your instance
"developer": username,
})
print_json(create_result)
print("\n")
if not create_result.get("success"):
print("Failed to create changeset. Exiting.")
sys.exit(1)
# Get the changeset ID from the create result
changeset_id = create_result["changeset"]["sys_id"]
print(f"Created changeset with ID: {changeset_id}")
print("\n")
# Step 3: Update the changeset
print("Step 3: Updating the changeset...")
update_result = update_changeset(auth_manager, server_config, {
"changeset_id": changeset_id,
"name": "Demo Changeset - Updated",
"description": "An updated demonstration changeset",
})
print_json(update_result)
print("\n")
# Step 4: Add a file to the changeset
print("Step 4: Adding a file to the changeset...")
file_content = """
function demoFunction() {
// This is a demonstration function
gs.info('Hello from the demo changeset!');
return 'Demo function executed successfully';
}
"""
add_file_result = add_file_to_changeset(auth_manager, server_config, {
"changeset_id": changeset_id,
"file_path": "scripts/demo_function.js",
"file_content": file_content,
})
print_json(add_file_result)
print("\n")
# Step 5: Get changeset details
print("Step 5: Getting changeset details...")
details_result = get_changeset_details(auth_manager, server_config, {
"changeset_id": changeset_id,
})
print_json(details_result)
print("\n")
# Step 6: Commit the changeset
print("Step 6: Committing the changeset...")
commit_result = commit_changeset(auth_manager, server_config, {
"changeset_id": changeset_id,
"commit_message": "Completed the demo changeset",
})
print_json(commit_result)
print("\n")
# Step 7: Publish the changeset
print("Step 7: Publishing the changeset...")
publish_result = publish_changeset(auth_manager, server_config, {
"changeset_id": changeset_id,
"publish_notes": "Demo changeset ready for deployment",
})
print_json(publish_result)
print("\n")
# Step 8: Get final changeset details
print("Step 8: Getting final changeset details...")
final_details_result = get_changeset_details(auth_manager, server_config, {
"changeset_id": changeset_id,
})
print_json(final_details_result)
print("\n")
print("=== Changeset Management Demo Completed ===")
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/src/servicenow_mcp/auth/auth_manager.py:
--------------------------------------------------------------------------------
```python
"""
Authentication manager for the ServiceNow MCP server.
"""
import base64
import logging
import os
from typing import Dict, Optional
import requests
from requests.auth import HTTPBasicAuth
from servicenow_mcp.utils.config import AuthConfig, AuthType
logger = logging.getLogger(__name__)
class AuthManager:
"""
Authentication manager for ServiceNow API.
This class handles authentication with the ServiceNow API using
different authentication methods.
"""
def __init__(self, config: AuthConfig, instance_url: str = None):
"""
Initialize the authentication manager.
Args:
config: Authentication configuration.
instance_url: ServiceNow instance URL.
"""
self.config = config
self.instance_url = instance_url
self.token: Optional[str] = None
self.token_type: Optional[str] = None
def get_headers(self) -> Dict[str, str]:
"""
Get the authentication headers for API requests.
Returns:
Dict[str, str]: Headers to include in API requests.
"""
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
if self.config.type == AuthType.BASIC:
if not self.config.basic:
raise ValueError("Basic auth configuration is required")
auth_str = f"{self.config.basic.username}:{self.config.basic.password}"
encoded = base64.b64encode(auth_str.encode()).decode()
headers["Authorization"] = f"Basic {encoded}"
elif self.config.type == AuthType.OAUTH:
if not self.token:
self._get_oauth_token()
headers["Authorization"] = f"{self.token_type} {self.token}"
elif self.config.type == AuthType.API_KEY:
if not self.config.api_key:
raise ValueError("API key configuration is required")
headers[self.config.api_key.header_name] = self.config.api_key.api_key
return headers
def _get_oauth_token(self):
"""
Get an OAuth token from ServiceNow.
Raises:
ValueError: If OAuth configuration is missing or token request fails.
"""
if not self.config.oauth:
raise ValueError("OAuth configuration is required")
oauth_config = self.config.oauth
# Determine token URL
token_url = oauth_config.token_url
if not token_url:
if not self.instance_url:
raise ValueError("Instance URL is required for OAuth authentication")
instance_parts = self.instance_url.split(".")
if len(instance_parts) < 2:
raise ValueError(f"Invalid instance URL: {self.instance_url}")
instance_name = instance_parts[0].split("//")[-1]
token_url = f"https://{instance_name}.service-now.com/oauth_token.do"
# Prepare Authorization header
auth_str = f"{oauth_config.client_id}:{oauth_config.client_secret}"
auth_header = base64.b64encode(auth_str.encode()).decode()
headers = {
"Authorization": f"Basic {auth_header}",
"Content-Type": "application/x-www-form-urlencoded"
}
# Try client_credentials grant first
data_client_credentials = {
"grant_type": "client_credentials"
}
logger.info("Attempting client_credentials grant...")
response = requests.post(token_url, headers=headers, data=data_client_credentials)
logger.info(f"client_credentials response status: {response.status_code}")
logger.info(f"client_credentials response body: {response.text}")
if response.status_code == 200:
token_data = response.json()
self.token = token_data.get("access_token")
self.token_type = token_data.get("token_type", "Bearer")
return
# Try password grant if client_credentials failed
if oauth_config.username and oauth_config.password:
data_password = {
"grant_type": "password",
"username": oauth_config.username,
"password": oauth_config.password
}
logger.info("Attempting password grant...")
response = requests.post(token_url, headers=headers, data=data_password)
logger.info(f"password grant response status: {response.status_code}")
logger.info(f"password grant response body: {response.text}")
if response.status_code == 200:
token_data = response.json()
self.token = token_data.get("access_token")
self.token_type = token_data.get("token_type", "Bearer")
return
raise ValueError("Failed to get OAuth token using both client_credentials and password grants.")
def refresh_token(self):
"""Refresh the OAuth token if using OAuth authentication."""
if self.config.type == AuthType.OAUTH:
self._get_oauth_token()
```
--------------------------------------------------------------------------------
/src/servicenow_mcp/tools/__init__.py:
--------------------------------------------------------------------------------
```python
"""
Tools module for the ServiceNow MCP server.
"""
# Import tools as they are implemented
from servicenow_mcp.tools.catalog_optimization import (
get_optimization_recommendations,
update_catalog_item,
)
from servicenow_mcp.tools.catalog_tools import (
create_catalog_category,
get_catalog_item,
list_catalog_categories,
list_catalog_items,
move_catalog_items,
update_catalog_category,
)
from servicenow_mcp.tools.catalog_variables import (
create_catalog_item_variable,
list_catalog_item_variables,
update_catalog_item_variable,
)
from servicenow_mcp.tools.change_tools import (
add_change_task,
approve_change,
create_change_request,
get_change_request_details,
list_change_requests,
reject_change,
submit_change_for_approval,
update_change_request,
)
from servicenow_mcp.tools.changeset_tools import (
add_file_to_changeset,
commit_changeset,
create_changeset,
get_changeset_details,
list_changesets,
publish_changeset,
update_changeset,
)
from servicenow_mcp.tools.incident_tools import (
add_comment,
create_incident,
list_incidents,
resolve_incident,
update_incident,
get_incident_by_number,
)
from servicenow_mcp.tools.knowledge_base import (
create_article,
create_category,
create_knowledge_base,
get_article,
list_articles,
list_knowledge_bases,
publish_article,
update_article,
list_categories,
)
from servicenow_mcp.tools.script_include_tools import (
create_script_include,
delete_script_include,
get_script_include,
list_script_includes,
update_script_include,
)
from servicenow_mcp.tools.user_tools import (
create_user,
update_user,
get_user,
list_users,
create_group,
update_group,
add_group_members,
remove_group_members,
list_groups,
)
from servicenow_mcp.tools.workflow_tools import (
activate_workflow,
add_workflow_activity,
create_workflow,
deactivate_workflow,
delete_workflow_activity,
get_workflow_activities,
get_workflow_details,
list_workflow_versions,
list_workflows,
reorder_workflow_activities,
update_workflow,
update_workflow_activity,
)
from servicenow_mcp.tools.story_tools import (
create_story,
update_story,
list_stories,
list_story_dependencies,
create_story_dependency,
delete_story_dependency,
)
from servicenow_mcp.tools.epic_tools import (
create_epic,
update_epic,
list_epics,
)
from servicenow_mcp.tools.scrum_task_tools import (
create_scrum_task,
update_scrum_task,
list_scrum_tasks,
)
from servicenow_mcp.tools.project_tools import (
create_project,
update_project,
list_projects,
)
# from servicenow_mcp.tools.problem_tools import create_problem, update_problem
# from servicenow_mcp.tools.request_tools import create_request, update_request
__all__ = [
# Incident tools
"create_incident",
"update_incident",
"add_comment",
"resolve_incident",
"list_incidents",
"get_incident_by_number",
# Catalog tools
"list_catalog_items",
"get_catalog_item",
"list_catalog_categories",
"create_catalog_category",
"update_catalog_category",
"move_catalog_items",
"get_optimization_recommendations",
"update_catalog_item",
"create_catalog_item_variable",
"list_catalog_item_variables",
"update_catalog_item_variable",
# Change management tools
"create_change_request",
"update_change_request",
"list_change_requests",
"get_change_request_details",
"add_change_task",
"submit_change_for_approval",
"approve_change",
"reject_change",
# Workflow management tools
"list_workflows",
"get_workflow_details",
"list_workflow_versions",
"get_workflow_activities",
"create_workflow",
"update_workflow",
"activate_workflow",
"deactivate_workflow",
"add_workflow_activity",
"update_workflow_activity",
"delete_workflow_activity",
"reorder_workflow_activities",
# Changeset tools
"list_changesets",
"get_changeset_details",
"create_changeset",
"update_changeset",
"commit_changeset",
"publish_changeset",
"add_file_to_changeset",
# Script Include tools
"list_script_includes",
"get_script_include",
"create_script_include",
"update_script_include",
"delete_script_include",
# Knowledge Base tools
"create_knowledge_base",
"list_knowledge_bases",
"create_category",
"list_categories",
"create_article",
"update_article",
"publish_article",
"list_articles",
"get_article",
# User management tools
"create_user",
"update_user",
"get_user",
"list_users",
"create_group",
"update_group",
"add_group_members",
"remove_group_members",
"list_groups",
# Story tools
"create_story",
"update_story",
"list_stories",
"list_story_dependencies",
"create_story_dependency",
"delete_story_dependency",
# Epic tools
"create_epic",
"update_epic",
"list_epics",
# Scrum Task tools
"create_scrum_task",
"update_scrum_task",
"list_scrum_tasks",
# Project tools
"create_project",
"update_project",
"list_projects",
# Future tools
# "create_problem",
# "update_problem",
# "create_request",
# "update_request",
]
```
--------------------------------------------------------------------------------
/docs/incident_management.md:
--------------------------------------------------------------------------------
```markdown
# Incident Management
This document describes the incident management functionality provided by the ServiceNow MCP server.
## Overview
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.
## Resources
### List Incidents
Retrieves a list of incidents from ServiceNow.
**Resource Name:** `incidents`
**Parameters:**
- `limit` (int, default: 10): Maximum number of incidents to return
- `offset` (int, default: 0): Offset for pagination
- `state` (string, optional): Filter by incident state
- `assigned_to` (string, optional): Filter by assigned user
- `category` (string, optional): Filter by category
- `query` (string, optional): Search query for incidents
**Example:**
```python
incidents = await mcp.get_resource("servicenow", "incidents", {
"limit": 5,
"state": "1", # New
"category": "Software"
})
for incident in incidents:
print(f"{incident.number}: {incident.short_description}")
```
### Get Incident
Retrieves a specific incident from ServiceNow by ID or number.
**Resource Name:** `incident`
**Parameters:**
- `incident_id` (string): Incident ID or sys_id
**Example:**
```python
incident = await mcp.get_resource("servicenow", "incident", "INC0010001")
print(f"Incident: {incident.number}")
print(f"Description: {incident.short_description}")
print(f"State: {incident.state}")
```
## Tools
### Create Incident
Creates a new incident in ServiceNow.
**Tool Name:** `create_incident`
**Parameters:**
- `short_description` (string, required): Short description of the incident
- `description` (string, optional): Detailed description of the incident
- `caller_id` (string, optional): User who reported the incident
- `category` (string, optional): Category of the incident
- `subcategory` (string, optional): Subcategory of the incident
- `priority` (string, optional): Priority of the incident
- `impact` (string, optional): Impact of the incident
- `urgency` (string, optional): Urgency of the incident
- `assigned_to` (string, optional): User assigned to the incident
- `assignment_group` (string, optional): Group assigned to the incident
**Example:**
```python
result = await mcp.use_tool("servicenow", "create_incident", {
"short_description": "Email service is down",
"description": "Users are unable to send or receive emails.",
"category": "Software",
"priority": "1"
})
print(f"Incident created: {result.incident_number}")
```
### Update Incident
Updates an existing incident in ServiceNow.
**Tool Name:** `update_incident`
**Parameters:**
- `incident_id` (string, required): Incident ID or sys_id
- `short_description` (string, optional): Short description of the incident
- `description` (string, optional): Detailed description of the incident
- `state` (string, optional): State of the incident
- `category` (string, optional): Category of the incident
- `subcategory` (string, optional): Subcategory of the incident
- `priority` (string, optional): Priority of the incident
- `impact` (string, optional): Impact of the incident
- `urgency` (string, optional): Urgency of the incident
- `assigned_to` (string, optional): User assigned to the incident
- `assignment_group` (string, optional): Group assigned to the incident
- `work_notes` (string, optional): Work notes to add to the incident
- `close_notes` (string, optional): Close notes to add to the incident
- `close_code` (string, optional): Close code for the incident
**Example:**
```python
result = await mcp.use_tool("servicenow", "update_incident", {
"incident_id": "INC0010001",
"priority": "2",
"assigned_to": "admin",
"work_notes": "Investigating the issue."
})
print(f"Incident updated: {result.success}")
```
### Add Comment
Adds a comment to an incident in ServiceNow.
**Tool Name:** `add_comment`
**Parameters:**
- `incident_id` (string, required): Incident ID or sys_id
- `comment` (string, required): Comment to add to the incident
- `is_work_note` (boolean, default: false): Whether the comment is a work note
**Example:**
```python
result = await mcp.use_tool("servicenow", "add_comment", {
"incident_id": "INC0010001",
"comment": "The issue is being investigated by the network team.",
"is_work_note": true
})
print(f"Comment added: {result.success}")
```
### Resolve Incident
Resolves an incident in ServiceNow.
**Tool Name:** `resolve_incident`
**Parameters:**
- `incident_id` (string, required): Incident ID or sys_id
- `resolution_code` (string, required): Resolution code for the incident
- `resolution_notes` (string, required): Resolution notes for the incident
**Example:**
```python
result = await mcp.use_tool("servicenow", "resolve_incident", {
"incident_id": "INC0010001",
"resolution_code": "Solved (Permanently)",
"resolution_notes": "The email service has been restored."
})
print(f"Incident resolved: {result.success}")
```
## State Values
ServiceNow incident states are represented by numeric values:
- `1`: New
- `2`: In Progress
- `3`: On Hold
- `4`: Resolved
- `5`: Closed
- `6`: Canceled
## Priority Values
ServiceNow incident priorities are represented by numeric values:
- `1`: Critical
- `2`: High
- `3`: Moderate
- `4`: Low
- `5`: Planning
## Testing
You can test the incident management functionality using the provided test script:
```bash
python examples/test_incidents.py
```
Make sure to set the required environment variables in your `.env` file:
```
SERVICENOW_INSTANCE_URL=https://your-instance.service-now.com
SERVICENOW_USERNAME=your-username
SERVICENOW_PASSWORD=your-password
SERVICENOW_AUTH_TYPE=basic
```
```
--------------------------------------------------------------------------------
/examples/catalog_integration_test.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
ServiceNow Catalog Integration Test
This script demonstrates how to use the ServiceNow MCP server to interact with
the ServiceNow Service Catalog. It serves as an integration test to verify that
the catalog functionality works correctly with a real ServiceNow instance.
Prerequisites:
1. Valid ServiceNow credentials with access to the Service Catalog
2. ServiceNow MCP package installed
Usage:
python examples/catalog_integration_test.py
"""
import os
import sys
from dotenv import load_dotenv
# Add the project root to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.tools.catalog_tools import (
GetCatalogItemParams,
ListCatalogCategoriesParams,
ListCatalogItemsParams,
get_catalog_item,
list_catalog_categories,
list_catalog_items,
)
from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
def main():
"""Run the catalog integration test."""
# Load environment variables
load_dotenv()
# Get configuration from environment variables
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
username = os.getenv("SERVICENOW_USERNAME")
password = os.getenv("SERVICENOW_PASSWORD")
if not instance_url or not username or not password:
print("Error: Missing required environment variables.")
print("Please set SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, and SERVICENOW_PASSWORD.")
sys.exit(1)
print(f"Connecting to ServiceNow instance: {instance_url}")
# Create the server config
config = ServerConfig(
instance_url=instance_url,
auth=AuthConfig(
type=AuthType.BASIC,
basic=BasicAuthConfig(username=username, password=password),
),
)
# Create the auth manager
auth_manager = AuthManager(config.auth)
# Test listing catalog categories
print("\n=== Testing List Catalog Categories ===")
category_id = test_list_catalog_categories(config, auth_manager)
# Test listing catalog items
print("\n=== Testing List Catalog Items ===")
item_id = test_list_catalog_items(config, auth_manager, category_id)
# Test getting a specific catalog item
if item_id:
print("\n=== Testing Get Catalog Item ===")
test_get_catalog_item(config, auth_manager, item_id)
def test_list_catalog_categories(config, auth_manager):
"""Test listing catalog categories."""
print("Fetching catalog categories...")
# Create the parameters
params = ListCatalogCategoriesParams(
limit=5,
offset=0,
query="",
active=True,
)
# Call the tool function directly
result = list_catalog_categories(config, auth_manager, params)
# Print the result
print(f"Found {result.get('total', 0)} catalog categories:")
for i, category in enumerate(result.get("categories", []), 1):
print(f"{i}. {category.get('title')} (ID: {category.get('sys_id')})")
print(f" Description: {category.get('description', 'N/A')}")
print()
# Save the first category ID for later use
if result.get("categories"):
return result["categories"][0]["sys_id"]
return None
def test_list_catalog_items(config, auth_manager, category_id=None):
"""Test listing catalog items."""
print("Fetching catalog items...")
# Create the parameters
params = ListCatalogItemsParams(
limit=5,
offset=0,
query="",
category=category_id, # Filter by category if provided
active=True,
)
# Call the tool function directly
result = list_catalog_items(config, auth_manager, params)
# Print the result
print(f"Found {result.get('total', 0)} catalog items:")
for i, item in enumerate(result.get("items", []), 1):
print(f"{i}. {item.get('name')} (ID: {item.get('sys_id')})")
print(f" Description: {item.get('short_description', 'N/A')}")
print(f" Category: {item.get('category', 'N/A')}")
print(f" Price: {item.get('price', 'N/A')}")
print()
# Save the first item ID for later use
if result.get("items"):
return result["items"][0]["sys_id"]
return None
def test_get_catalog_item(config, auth_manager, item_id):
"""Test getting a specific catalog item."""
print(f"Fetching details for catalog item: {item_id}")
# Create the parameters
params = GetCatalogItemParams(
item_id=item_id,
)
# Call the tool function directly
result = get_catalog_item(config, auth_manager, params)
# Print the result
if result.success:
print(f"Retrieved catalog item: {result.data.get('name')} (ID: {result.data.get('sys_id')})")
print(f"Description: {result.data.get('description', 'N/A')}")
print(f"Category: {result.data.get('category', 'N/A')}")
print(f"Price: {result.data.get('price', 'N/A')}")
print(f"Delivery Time: {result.data.get('delivery_time', 'N/A')}")
print(f"Availability: {result.data.get('availability', 'N/A')}")
# Print variables
variables = result.data.get("variables", [])
if variables:
print("\nVariables:")
for i, variable in enumerate(variables, 1):
print(f"{i}. {variable.get('label')} ({variable.get('name')})")
print(f" Type: {variable.get('type')}")
print(f" Mandatory: {variable.get('mandatory')}")
print(f" Default Value: {variable.get('default_value', 'N/A')}")
print()
else:
print(f"Error: {result.message}")
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/config/tool_packages.yaml:
--------------------------------------------------------------------------------
```yaml
# ServiceNow MCP Tool Packages Configuration
# Define sets of tools to be exposed by the MCP server based on roles/workflows.
# The MCP server loads tools based on the MCP_TOOL_PACKAGE environment variable.
# If MCP_TOOL_PACKAGE is not set, 'full' is loaded.
# If MCP_TOOL_PACKAGE is set to 'none', no tools are loaded (except list_tool_packages).
# --- Package Definitions ---
none: []
service_desk:
# Incident Management
- create_incident
- update_incident
- add_comment
- resolve_incident
- list_incidents
- get_incident_by_number
# User Lookup
- get_user
- list_users
# Knowledge Lookup
- get_article
- list_articles
catalog_builder:
# Core Catalog
- list_catalogs
- list_catalog_items
- get_catalog_item
- create_catalog_item
- update_catalog_item
# Categories
- list_catalog_categories
- create_catalog_category
- update_catalog_category
- move_catalog_items
# Variables
- create_catalog_item_variable
- list_catalog_item_variables
- update_catalog_item_variable
- delete_catalog_item_variable
- create_catalog_variable_choice
# Basic Scripting (UI Policy & User Criteria)
- create_ui_policy
- create_ui_policy_action
- create_user_criteria
- create_user_criteria_condition
# Optimization
- get_optimization_recommendations
change_coordinator:
# Change Lifecycle
- create_change_request
- update_change_request
- list_change_requests
- get_change_request_details
# Tasks & Approval
- add_change_task
- submit_change_for_approval
- approve_change
- reject_change
knowledge_author:
# KB Management
- create_knowledge_base
- list_knowledge_bases
# KB Categories
- create_category # KB specific category tool
- list_categories # KB specific category tool
# Article Lifecycle
- create_article
- update_article
- publish_article
- list_articles
- get_article
platform_developer:
# Script Includes
- list_script_includes
- get_script_include
- create_script_include
- update_script_include
- delete_script_include
- execute_script_include
# Workflows
- list_workflows
- get_workflow_details
- create_workflow
- update_workflow
- activate_workflow
- deactivate_workflow
- list_workflow_versions
# Workflow Activities
- add_workflow_activity
- update_workflow_activity
- delete_workflow_activity
- reorder_workflow_activities
- get_workflow_activities
# General UI Policies
- create_ui_policy
- create_ui_policy_action
# Changesets
- list_changesets
- get_changeset_details
- create_changeset
- update_changeset
- commit_changeset
- publish_changeset
- add_file_to_changeset
agile_management:
# Story Management
- create_story
- update_story
- list_stories
- list_story_dependencies
- create_story_dependency
- delete_story_dependency
# Epic Management
- create_epic
- update_epic
- list_epics
# Scrum Task Management
- create_scrum_task
- update_scrum_task
- list_scrum_tasks
# Project Management
- create_project
- update_project
- list_projects
system_administrator:
# User Management
- create_user
- update_user
- get_user
- list_users
# Group Management
- create_group
- update_group
- list_groups
- add_group_members
- remove_group_members
# System Logs
- list_syslog_entries
- get_syslog_entry
full:
# Incident Management
- create_incident
- update_incident
- add_comment
- resolve_incident
- list_incidents
- get_incident_by_number
# Catalog (Core)
- list_catalogs
- list_catalog_items
- get_catalog_item
- create_catalog_item
- update_catalog_item
# Catalog (Categories)
- list_catalog_categories
- create_catalog_category
- update_catalog_category
- move_catalog_items
# Catalog (Optimization)
- get_optimization_recommendations
# Catalog (Variables)
- create_catalog_item_variable
- list_catalog_item_variables
- update_catalog_item_variable
- delete_catalog_item_variable
- create_catalog_variable_choice
# Change Management
- create_change_request
- update_change_request
- list_change_requests
- get_change_request_details
- add_change_task
- submit_change_for_approval
- approve_change
- reject_change
# Workflow
- list_workflows
- get_workflow_details
- list_workflow_versions
- get_workflow_activities
- create_workflow
- update_workflow
- activate_workflow
- deactivate_workflow
- add_workflow_activity
- update_workflow_activity
- delete_workflow_activity
- reorder_workflow_activities
# Changeset
- list_changesets
- get_changeset_details
- create_changeset
- update_changeset
- commit_changeset
- publish_changeset
- add_file_to_changeset
# Script Include
- list_script_includes
- get_script_include
- create_script_include
- update_script_include
- delete_script_include
- execute_script_include
# Knowledge Base
- create_knowledge_base
- list_knowledge_bases
- create_category # KB specific
- list_categories # KB specific
- create_article
- update_article
- publish_article
- list_articles
- get_article
# User Management
- create_user
- update_user
- get_user
- list_users
- create_group
- update_group
- add_group_members
- remove_group_members
- list_groups
# Syslog
- list_syslog_entries
- get_syslog_entry
# Catalog Criteria
- create_user_criteria
- create_user_criteria_condition
# UI Policy
- create_ui_policy
- create_ui_policy_action
# Story Management
- create_story
- update_story
- list_stories
- list_story_dependencies
- create_story_dependency
- delete_story_dependency
# Epic Management
- create_epic
- update_epic
- list_epics
# Scrum Task Management
- create_scrum_task
- update_scrum_task
- list_scrum_tasks
# Project Management
- create_project
- update_project
- list_projects
```
--------------------------------------------------------------------------------
/docs/changeset_management.md:
--------------------------------------------------------------------------------
```markdown
# Changeset Management in ServiceNow MCP
This document provides detailed information about the Changeset Management tools available in the ServiceNow MCP server.
## Overview
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).
The ServiceNow MCP server provides tools for managing changesets, allowing Claude to help with:
- Tracking development changes
- Creating and managing changesets
- Committing and publishing changesets
- Adding files to changesets
- Analyzing changeset contents
## Available Tools
### 1. list_changesets
Lists changesets from ServiceNow with various filtering options.
**Parameters:**
- `limit` (optional, default: 10) - Maximum number of records to return
- `offset` (optional, default: 0) - Offset to start from
- `state` (optional) - Filter by state (e.g., "in_progress", "complete", "published")
- `application` (optional) - Filter by application
- `developer` (optional) - Filter by developer
- `timeframe` (optional) - Filter by timeframe ("recent", "last_week", "last_month")
- `query` (optional) - Additional query string
**Example:**
```python
result = list_changesets({
"limit": 20,
"state": "in_progress",
"developer": "john.doe",
"timeframe": "recent"
})
```
### 2. get_changeset_details
Gets detailed information about a specific changeset, including all changes contained within it.
**Parameters:**
- `changeset_id` (required) - Changeset ID or sys_id
**Example:**
```python
result = get_changeset_details({
"changeset_id": "sys_update_set_123"
})
```
### 3. create_changeset
Creates a new changeset in ServiceNow.
**Parameters:**
- `name` (required) - Name of the changeset
- `description` (optional) - Description of the changeset
- `application` (required) - Application the changeset belongs to
- `developer` (optional) - Developer responsible for the changeset
**Example:**
```python
result = create_changeset({
"name": "HR Portal Login Fix",
"description": "Fixes the login issue on the HR Portal",
"application": "HR Portal",
"developer": "john.doe"
})
```
### 4. update_changeset
Updates an existing changeset in ServiceNow.
**Parameters:**
- `changeset_id` (required) - Changeset ID or sys_id
- `name` (optional) - Name of the changeset
- `description` (optional) - Description of the changeset
- `state` (optional) - State of the changeset
- `developer` (optional) - Developer responsible for the changeset
**Example:**
```python
result = update_changeset({
"changeset_id": "sys_update_set_123",
"name": "HR Portal Login Fix - Updated",
"description": "Updated description for the login fix",
"state": "in_progress"
})
```
### 5. commit_changeset
Commits a changeset in ServiceNow, marking it as complete.
**Parameters:**
- `changeset_id` (required) - Changeset ID or sys_id
- `commit_message` (optional) - Commit message
**Example:**
```python
result = commit_changeset({
"changeset_id": "sys_update_set_123",
"commit_message": "Completed the login fix with all necessary changes"
})
```
### 6. publish_changeset
Publishes a changeset in ServiceNow, making it available for deployment to other environments.
**Parameters:**
- `changeset_id` (required) - Changeset ID or sys_id
- `publish_notes` (optional) - Notes for publishing
**Example:**
```python
result = publish_changeset({
"changeset_id": "sys_update_set_123",
"publish_notes": "Ready for deployment to test environment"
})
```
### 7. add_file_to_changeset
Adds a file to a changeset in ServiceNow.
**Parameters:**
- `changeset_id` (required) - Changeset ID or sys_id
- `file_path` (required) - Path of the file to add
- `file_content` (required) - Content of the file
**Example:**
```python
result = add_file_to_changeset({
"changeset_id": "sys_update_set_123",
"file_path": "scripts/login_fix.js",
"file_content": "function fixLogin() { ... }"
})
```
## Resources
The ServiceNow MCP server also provides the following resources for accessing changesets:
### 1. changesets://list
URI for listing changesets from ServiceNow.
**Example:**
```
changesets://list
```
### 2. changeset://{changeset_id}
URI for getting a specific changeset from ServiceNow by ID.
**Example:**
```
changeset://sys_update_set_123
```
## Changeset States
Changesets in ServiceNow typically go through the following states:
1. **in_progress** - The changeset is being actively worked on
2. **complete** - The changeset has been completed and is ready for review
3. **published** - The changeset has been published and is ready for deployment
4. **deployed** - The changeset has been deployed to another environment
## Best Practices
1. **Naming Convention**: Use a consistent naming convention for changesets that includes the application name, feature/fix description, and optionally a ticket number.
2. **Scope**: Keep changesets focused on a single feature, fix, or improvement to make them easier to review and deploy.
3. **Documentation**: Include detailed descriptions for changesets to help reviewers understand the purpose and impact of the changes.
4. **Testing**: Test all changes thoroughly before committing and publishing a changeset.
5. **Review**: Have changesets reviewed by another developer before publishing to catch potential issues.
6. **Backup**: Always back up important configurations before deploying changesets to production.
## Example Workflow
1. Create a new changeset for a specific feature or fix
2. Make the necessary changes in ServiceNow
3. Add any required files to the changeset
4. Test the changes thoroughly
5. Commit the changeset with a detailed message
6. Have the changeset reviewed
7. Publish the changeset
8. Deploy the changeset to the target environment
## Troubleshooting
### Common Issues
1. **Changeset Conflicts**: When multiple developers modify the same configuration item, conflicts can occur. Resolve these by carefully reviewing and merging the changes.
2. **Missing Dependencies**: Changesets may depend on other configurations that aren't included. Ensure all dependencies are identified and included.
3. **Deployment Failures**: If a changeset fails to deploy, check the deployment logs for specific errors and address them before retrying.
4. **Permission Issues**: Ensure the user has the necessary permissions to create, commit, and publish changesets.
### Error Messages
- **"Cannot find changeset"**: The specified changeset ID does not exist or is not accessible.
- **"Missing required fields"**: One or more required parameters are missing.
- **"Invalid state transition"**: Attempting to change the state of a changeset in an invalid way (e.g., from "in_progress" directly to "published").
- **"Application not found"**: The specified application does not exist or is not accessible.
```
--------------------------------------------------------------------------------
/scripts/check_pdi_info.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
ServiceNow PDI Information Checker
This script helps determine the correct credentials for a ServiceNow PDI instance.
It will:
1. Check if the instance is reachable
2. Try different common username combinations
3. Provide guidance on finding the correct credentials
Usage:
python scripts/check_pdi_info.py
"""
import os
import sys
import requests
from pathlib import Path
from getpass import getpass
from dotenv import load_dotenv
# Add the project root to the Python path
sys.path.insert(0, str(Path(__file__).parent.parent))
def check_instance_info(instance_url):
"""Check basic information about the ServiceNow instance."""
print(f"\nChecking instance: {instance_url}")
# Check if the instance is reachable
try:
response = requests.get(f"{instance_url}/api/now/table/sys_properties?sysparm_limit=1",
headers={"Accept": "application/json"})
if response.status_code == 200:
print("✅ Instance is reachable")
print("❌ But authentication is required")
elif response.status_code == 401:
print("✅ Instance is reachable")
print("❌ Authentication required")
else:
print(f"❌ Instance returned unexpected status code: {response.status_code}")
print(f"Response: {response.text}")
except requests.exceptions.RequestException as e:
print(f"❌ Error connecting to instance: {e}")
return False
return True
def test_credentials(instance_url, username, password):
"""Test a set of credentials against the ServiceNow instance."""
print(f"\nTesting credentials: {username} / {'*' * len(password)}")
try:
response = requests.get(
f"{instance_url}/api/now/table/incident?sysparm_limit=1",
auth=(username, password),
headers={"Accept": "application/json"}
)
if response.status_code == 200:
print("✅ Authentication successful!")
data = response.json()
print(f"Retrieved {len(data.get('result', []))} incident(s)")
return True
else:
print(f"❌ Authentication failed with status code: {response.status_code}")
print(f"Response: {response.text}")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Connection error: {e}")
return False
def main():
"""Main function to run the PDI checker."""
load_dotenv()
print("=" * 60)
print("ServiceNow PDI Credential Checker".center(60))
print("=" * 60)
# Get instance URL
instance_url = os.getenv("SERVICENOW_INSTANCE_URL")
if not instance_url or instance_url == "https://your-instance.service-now.com":
instance_url = input("Enter your ServiceNow instance URL: ")
# Check if instance is reachable
if not check_instance_info(instance_url):
print("\nPlease check your instance URL and try again.")
return
print("\nFor a Personal Developer Instance (PDI), try these common usernames:")
print("1. admin")
print("2. Your ServiceNow account email")
print("3. Your ServiceNow username (without @domain.com)")
# Try with current credentials first
current_username = os.getenv("SERVICENOW_USERNAME")
current_password = os.getenv("SERVICENOW_PASSWORD")
if current_username and current_password:
print("\nTrying with current credentials from .env file...")
if test_credentials(instance_url, current_username, current_password):
print("\n✅ Your current credentials in .env are working!")
return
# Ask for new credentials
print("\nLet's try with new credentials:")
# Try admin
print("\nTrying with 'admin' username...")
admin_password = getpass("Enter password for 'admin' user (press Enter to skip): ")
if admin_password and test_credentials(instance_url, "admin", admin_password):
update = input("\nDo you want to update your .env file with these credentials? (y/n): ")
if update.lower() == 'y':
update_env_file(instance_url, "admin", admin_password)
return
# Try with email
email = input("\nEnter your ServiceNow account email: ")
if email:
email_password = getpass(f"Enter password for '{email}': ")
if test_credentials(instance_url, email, email_password):
update = input("\nDo you want to update your .env file with these credentials? (y/n): ")
if update.lower() == 'y':
update_env_file(instance_url, email, email_password)
return
# Try with username only (no domain)
if '@' in email:
username = email.split('@')[0]
print(f"\nTrying with username part only: '{username}'...")
username_password = getpass(f"Enter password for '{username}': ")
if test_credentials(instance_url, username, username_password):
update = input("\nDo you want to update your .env file with these credentials? (y/n): ")
if update.lower() == 'y':
update_env_file(instance_url, username, username_password)
return
print("\n❌ None of the common credential combinations worked.")
print("\nTo find your PDI credentials:")
print("1. Go to https://developer.servicenow.com/")
print("2. Log in with your ServiceNow account")
print("3. Go to 'My Instances'")
print("4. Find your PDI and click on it")
print("5. Look for the credentials information")
# Ask for custom credentials
custom = input("\nDo you want to try custom credentials? (y/n): ")
if custom.lower() == 'y':
custom_username = input("Enter username: ")
custom_password = getpass("Enter password: ")
if test_credentials(instance_url, custom_username, custom_password):
update = input("\nDo you want to update your .env file with these credentials? (y/n): ")
if update.lower() == 'y':
update_env_file(instance_url, custom_username, custom_password)
def update_env_file(instance_url, username, password):
"""Update the .env file with working credentials."""
env_path = Path(__file__).parent.parent / '.env'
with open(env_path, 'r') as f:
env_content = f.read()
# Update credentials
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}')
env_content = env_content.replace(f'SERVICENOW_USERNAME={os.getenv("SERVICENOW_USERNAME", "your-username")}', f'SERVICENOW_USERNAME={username}')
env_content = env_content.replace(f'SERVICENOW_PASSWORD={os.getenv("SERVICENOW_PASSWORD", "your-password")}', f'SERVICENOW_PASSWORD={password}')
with open(env_path, 'w') as f:
f.write(env_content)
print("✅ Updated .env file with working credentials!")
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/docs/catalog_variables.md:
--------------------------------------------------------------------------------
```markdown
# Catalog Item Variables in ServiceNow MCP
This document describes the tools available for managing variables (form fields) in ServiceNow catalog items using the ServiceNow MCP server.
## Overview
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.
## Available Tools
### create_catalog_item_variable
Creates a new variable (form field) for a catalog item.
#### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| catalog_item_id | string | Yes | The sys_id of the catalog item |
| name | string | Yes | The name of the variable (internal name) |
| type | string | Yes | The type of variable (e.g., string, integer, boolean, reference) |
| label | string | Yes | The display label for the variable |
| mandatory | boolean | No | Whether the variable is required (default: false) |
| help_text | string | No | Help text to display with the variable |
| default_value | string | No | Default value for the variable |
| description | string | No | Description of the variable |
| order | integer | No | Display order of the variable |
| reference_table | string | No | For reference fields, the table to reference |
| reference_qualifier | string | No | For reference fields, the query to filter reference options |
| max_length | integer | No | Maximum length for string fields |
| min | integer | No | Minimum value for numeric fields |
| max | integer | No | Maximum value for numeric fields |
#### Example
```python
result = create_catalog_item_variable({
"catalog_item_id": "item123",
"name": "requested_for",
"type": "reference",
"label": "Requested For",
"mandatory": true,
"reference_table": "sys_user",
"reference_qualifier": "active=true",
"help_text": "User who needs this item"
})
```
#### Response
| Field | Type | Description |
|-------|------|-------------|
| success | boolean | Whether the operation was successful |
| message | string | A message describing the result |
| variable_id | string | The sys_id of the created variable |
| details | object | Additional details about the variable |
### list_catalog_item_variables
Lists all variables (form fields) for a catalog item.
#### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| catalog_item_id | string | Yes | The sys_id of the catalog item |
| include_details | boolean | No | Whether to include detailed information about each variable (default: true) |
| limit | integer | No | Maximum number of variables to return |
| offset | integer | No | Offset for pagination |
#### Example
```python
result = list_catalog_item_variables({
"catalog_item_id": "item123",
"include_details": true,
"limit": 10,
"offset": 0
})
```
#### Response
| Field | Type | Description |
|-------|------|-------------|
| success | boolean | Whether the operation was successful |
| message | string | A message describing the result |
| variables | array | List of variables |
| count | integer | Total number of variables found |
### update_catalog_item_variable
Updates an existing variable (form field) for a catalog item.
#### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| variable_id | string | Yes | The sys_id of the variable to update |
| label | string | No | The display label for the variable |
| mandatory | boolean | No | Whether the variable is required |
| help_text | string | No | Help text to display with the variable |
| default_value | string | No | Default value for the variable |
| description | string | No | Description of the variable |
| order | integer | No | Display order of the variable |
| reference_qualifier | string | No | For reference fields, the query to filter reference options |
| max_length | integer | No | Maximum length for string fields |
| min | integer | No | Minimum value for numeric fields |
| max | integer | No | Maximum value for numeric fields |
#### Example
```python
result = update_catalog_item_variable({
"variable_id": "var123",
"label": "Updated Label",
"mandatory": true,
"help_text": "New help text",
"order": 200
})
```
#### Response
| Field | Type | Description |
|-------|------|-------------|
| success | boolean | Whether the operation was successful |
| message | string | A message describing the result |
| variable_id | string | The sys_id of the updated variable |
| details | object | Additional details about the variable |
## Common Variable Types
ServiceNow supports various variable types:
| Type | Description |
|------|-------------|
| string | Single-line text field |
| multi_line_text | Multi-line text area |
| integer | Whole number field |
| float | Decimal number field |
| boolean | Checkbox (true/false) |
| choice | Dropdown menu or radio buttons |
| reference | Reference to another record |
| date | Date picker |
| datetime | Date and time picker |
| password | Password field (masked input) |
| email | Email address field |
| url | URL field |
| currency | Currency field |
| html | Rich text editor |
| upload | File attachment |
## Example Usage with Claude
Once the ServiceNow MCP server is configured with Claude, you can ask Claude to perform actions like:
- "Create a description field for the laptop request catalog item"
- "Add a dropdown field for selecting laptop models to catalog item sys_id_123"
- "List all variables for the VPN access request catalog item"
- "Make the department field mandatory in the software request form"
- "Update the help text for the cost center field in the travel request form"
- "Add a reference field to the computer request form that lets users select their manager"
- "Show all variables for the new hire onboarding catalog item in order"
- "Change the order of fields in the hardware request form to put name first"
## Troubleshooting
### Common Errors
1. **Error: `Mandatory variable not provided`**
- This error occurs when you don't provide all required parameters when creating a variable.
- Solution: Make sure to include all required parameters: `catalog_item_id`, `name`, `type`, and `label`.
2. **Error: `Invalid variable type specified`**
- This error occurs when you provide an invalid value for the `type` parameter.
- Solution: Use one of the valid variable types listed in the "Common Variable Types" section.
3. **Error: `Reference table required for reference variables`**
- This error occurs when creating a reference-type variable without specifying the `reference_table`.
- Solution: Always include the `reference_table` parameter when creating reference-type variables.
4. **Error: `No update parameters provided`**
- This error occurs when calling `update_catalog_item_variable` with only the variable_id and no other parameters.
- Solution: Provide at least one field to update.
```
--------------------------------------------------------------------------------
/tests/test_workflow_tools_params.py:
--------------------------------------------------------------------------------
```python
import unittest
from unittest.mock import MagicMock, patch
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.utils.config import ServerConfig
from servicenow_mcp.tools.workflow_tools import (
_get_auth_and_config,
list_workflows,
get_workflow_details,
create_workflow,
)
class TestWorkflowToolsParams(unittest.TestCase):
"""Test parameter handling in workflow tools."""
def setUp(self):
"""Set up test fixtures."""
# Create mock objects for AuthManager and ServerConfig
self.auth_manager = MagicMock(spec=AuthManager)
self.auth_manager.get_headers.return_value = {"Authorization": "Bearer test-token"}
self.server_config = MagicMock(spec=ServerConfig)
self.server_config.instance_url = "https://test-instance.service-now.com"
def test_get_auth_and_config_correct_order(self):
"""Test _get_auth_and_config with parameters in the correct order."""
auth, config = _get_auth_and_config(self.auth_manager, self.server_config)
self.assertEqual(auth, self.auth_manager)
self.assertEqual(config, self.server_config)
def test_get_auth_and_config_swapped_order(self):
"""Test _get_auth_and_config with parameters in the swapped order."""
auth, config = _get_auth_and_config(self.server_config, self.auth_manager)
self.assertEqual(auth, self.auth_manager)
self.assertEqual(config, self.server_config)
def test_get_auth_and_config_error_handling(self):
"""Test _get_auth_and_config error handling with invalid parameters."""
# Create objects that don't have the required attributes
invalid_obj1 = MagicMock()
# Explicitly remove attributes to ensure they don't exist
del invalid_obj1.get_headers
del invalid_obj1.instance_url
invalid_obj2 = MagicMock()
# Explicitly remove attributes to ensure they don't exist
del invalid_obj2.get_headers
del invalid_obj2.instance_url
with self.assertRaises(ValueError):
_get_auth_and_config(invalid_obj1, invalid_obj2)
@patch('servicenow_mcp.tools.workflow_tools.requests.get')
def test_list_workflows_correct_params(self, mock_get):
"""Test list_workflows with parameters in the correct order."""
# Setup mock response
mock_response = MagicMock()
mock_response.json.return_value = {"result": [{"sys_id": "123", "name": "Test Workflow"}]}
mock_response.headers = {"X-Total-Count": "1"}
mock_get.return_value = mock_response
# Call the function
result = list_workflows(self.auth_manager, self.server_config, {"limit": 10})
# Verify the function called requests.get with the correct parameters
mock_get.assert_called_once()
self.assertEqual(result["count"], 1)
self.assertEqual(result["workflows"][0]["name"], "Test Workflow")
@patch('servicenow_mcp.tools.workflow_tools.requests.get')
def test_list_workflows_swapped_params(self, mock_get):
"""Test list_workflows with parameters in the swapped order."""
# Setup mock response
mock_response = MagicMock()
mock_response.json.return_value = {"result": [{"sys_id": "123", "name": "Test Workflow"}]}
mock_response.headers = {"X-Total-Count": "1"}
mock_get.return_value = mock_response
# Call the function with swapped parameters
result = list_workflows(self.server_config, self.auth_manager, {"limit": 10})
# Verify the function still works correctly
mock_get.assert_called_once()
self.assertEqual(result["count"], 1)
self.assertEqual(result["workflows"][0]["name"], "Test Workflow")
@patch('servicenow_mcp.tools.workflow_tools.requests.get')
def test_get_workflow_details_correct_params(self, mock_get):
"""Test get_workflow_details with parameters in the correct order."""
# Setup mock response
mock_response = MagicMock()
mock_response.json.return_value = {"result": {"sys_id": "123", "name": "Test Workflow"}}
mock_get.return_value = mock_response
# Call the function
result = get_workflow_details(self.auth_manager, self.server_config, {"workflow_id": "123"})
# Verify the function called requests.get with the correct parameters
mock_get.assert_called_once()
self.assertEqual(result["workflow"]["name"], "Test Workflow")
@patch('servicenow_mcp.tools.workflow_tools.requests.get')
def test_get_workflow_details_swapped_params(self, mock_get):
"""Test get_workflow_details with parameters in the swapped order."""
# Setup mock response
mock_response = MagicMock()
mock_response.json.return_value = {"result": {"sys_id": "123", "name": "Test Workflow"}}
mock_get.return_value = mock_response
# Call the function with swapped parameters
result = get_workflow_details(self.server_config, self.auth_manager, {"workflow_id": "123"})
# Verify the function still works correctly
mock_get.assert_called_once()
self.assertEqual(result["workflow"]["name"], "Test Workflow")
@patch('servicenow_mcp.tools.workflow_tools.requests.post')
def test_create_workflow_correct_params(self, mock_post):
"""Test create_workflow with parameters in the correct order."""
# Setup mock response
mock_response = MagicMock()
mock_response.json.return_value = {"result": {"sys_id": "123", "name": "New Workflow"}}
mock_post.return_value = mock_response
# Call the function
result = create_workflow(
self.auth_manager,
self.server_config,
{"name": "New Workflow", "description": "Test description"}
)
# Verify the function called requests.post with the correct parameters
mock_post.assert_called_once()
self.assertEqual(result["workflow"]["name"], "New Workflow")
self.assertEqual(result["message"], "Workflow created successfully")
@patch('servicenow_mcp.tools.workflow_tools.requests.post')
def test_create_workflow_swapped_params(self, mock_post):
"""Test create_workflow with parameters in the swapped order."""
# Setup mock response
mock_response = MagicMock()
mock_response.json.return_value = {"result": {"sys_id": "123", "name": "New Workflow"}}
mock_post.return_value = mock_response
# Call the function with swapped parameters
result = create_workflow(
self.server_config,
self.auth_manager,
{"name": "New Workflow", "description": "Test description"}
)
# Verify the function still works correctly
mock_post.assert_called_once()
self.assertEqual(result["workflow"]["name"], "New Workflow")
self.assertEqual(result["message"], "Workflow created successfully")
if __name__ == '__main__':
unittest.main()
```
--------------------------------------------------------------------------------
/examples/change_management_demo.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python
"""
Change Management Demo
This script demonstrates how to use the ServiceNow MCP server with Claude Desktop
to manage change requests in ServiceNow.
"""
import json
import os
from datetime import datetime, timedelta
from dotenv import load_dotenv
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.tools.change_tools import (
add_change_task,
approve_change,
create_change_request,
get_change_request_details,
list_change_requests,
submit_change_for_approval,
update_change_request,
)
from servicenow_mcp.utils.config import AuthConfig, AuthType, BasicAuthConfig, ServerConfig
def main():
"""Run the change management demo."""
# Load environment variables from .env file
load_dotenv()
# Create auth configuration
auth_config = AuthConfig(
type=AuthType.BASIC,
basic=BasicAuthConfig(
username=os.environ.get("SERVICENOW_USERNAME"),
password=os.environ.get("SERVICENOW_PASSWORD"),
)
)
# Create server configuration with auth
server_config = ServerConfig(
instance_url=os.environ.get("SERVICENOW_INSTANCE_URL"),
debug=os.environ.get("DEBUG", "false").lower() == "true",
auth=auth_config,
)
# Create authentication manager with the auth_config
auth_manager = AuthManager(auth_config)
print("ServiceNow Change Management Demo")
print("=================================")
print(f"Instance URL: {server_config.instance_url}")
print(f"Auth Type: {auth_config.type}")
print()
# Demo 1: Create a change request
print("Demo 1: Creating a change request")
print("---------------------------------")
# Calculate start and end dates for the change (tomorrow)
tomorrow = datetime.now() + timedelta(days=1)
start_date = tomorrow.replace(hour=1, minute=0, second=0).strftime("%Y-%m-%d %H:%M:%S")
end_date = tomorrow.replace(hour=3, minute=0, second=0).strftime("%Y-%m-%d %H:%M:%S")
create_params = {
"short_description": "Server maintenance - Apply security patches",
"description": "Apply the latest security patches to the application servers to address recent vulnerabilities.",
"type": "normal",
"risk": "moderate",
"impact": "medium",
"category": "Hardware",
"start_date": start_date,
"end_date": end_date,
}
result = create_change_request(auth_manager, server_config, create_params)
print(json.dumps(result, indent=2))
if not result.get("success"):
print("Failed to create change request. Exiting demo.")
return
# Store the change request ID for later use
change_id = result["change_request"]["sys_id"]
print(f"Created change request with ID: {change_id}")
print()
# Demo 2: Add tasks to the change request
print("Demo 2: Adding tasks to the change request")
print("-----------------------------------------")
# Task 1: Pre-implementation checks
task1_params = {
"change_id": change_id,
"short_description": "Pre-implementation checks",
"description": "Verify system backups and confirm all prerequisites are met.",
"planned_start_date": start_date,
"planned_end_date": (datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") + timedelta(minutes=30)).strftime("%Y-%m-%d %H:%M:%S"),
}
task1_result = add_change_task(auth_manager, server_config, task1_params)
print(json.dumps(task1_result, indent=2))
# Task 2: Apply patches
task2_params = {
"change_id": change_id,
"short_description": "Apply security patches",
"description": "Apply the latest security patches to all application servers.",
"planned_start_date": (datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") + timedelta(minutes=30)).strftime("%Y-%m-%d %H:%M:%S"),
"planned_end_date": (datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") + timedelta(minutes=90)).strftime("%Y-%m-%d %H:%M:%S"),
}
task2_result = add_change_task(auth_manager, server_config, task2_params)
print(json.dumps(task2_result, indent=2))
# Task 3: Post-implementation verification
task3_params = {
"change_id": change_id,
"short_description": "Post-implementation verification",
"description": "Verify all systems are functioning correctly after patching.",
"planned_start_date": (datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") + timedelta(minutes=90)).strftime("%Y-%m-%d %H:%M:%S"),
"planned_end_date": (datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") + timedelta(minutes=120)).strftime("%Y-%m-%d %H:%M:%S"),
}
task3_result = add_change_task(auth_manager, server_config, task3_params)
print(json.dumps(task3_result, indent=2))
print()
# Demo 3: Update the change request
print("Demo 3: Updating the change request")
print("----------------------------------")
update_params = {
"change_id": change_id,
"work_notes": "Added implementation plan and tasks. Ready for review.",
"risk": "low", # Downgrading risk after assessment
}
update_result = update_change_request(auth_manager, server_config, update_params)
print(json.dumps(update_result, indent=2))
print()
# Demo 4: Get change request details
print("Demo 4: Getting change request details")
print("-------------------------------------")
details_params = {
"change_id": change_id,
}
details_result = get_change_request_details(auth_manager, server_config, details_params)
print(json.dumps(details_result, indent=2))
print()
# Demo 5: Submit change for approval
print("Demo 5: Submitting change for approval")
print("-------------------------------------")
approval_params = {
"change_id": change_id,
"approval_comments": "Implementation plan is complete and ready for review.",
}
approval_result = submit_change_for_approval(auth_manager, server_config, approval_params)
print(json.dumps(approval_result, indent=2))
print()
# Demo 6: List change requests
print("Demo 6: Listing change requests")
print("------------------------------")
list_params = {
"limit": 5,
"timeframe": "upcoming",
}
list_result = list_change_requests(auth_manager, server_config, list_params)
print(json.dumps(list_result, indent=2))
print()
# Demo 7: Approve the change request
print("Demo 7: Approving the change request")
print("-----------------------------------")
approve_params = {
"change_id": change_id,
"approval_comments": "Implementation plan looks good. Approved.",
}
approve_result = approve_change(auth_manager, server_config, approve_params)
print(json.dumps(approve_result, indent=2))
print()
print("Change Management Demo Complete")
print("==============================")
print(f"Created and managed change request: {change_id}")
print("You can now view this change request in your ServiceNow instance.")
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/docs/change_management.md:
--------------------------------------------------------------------------------
```markdown
# ServiceNow MCP Change Management Tools
This document provides information about the change management tools available in the ServiceNow MCP server.
## Overview
The change management tools allow Claude to interact with ServiceNow's change management functionality, enabling users to create, update, and manage change requests through natural language conversations.
## Available Tools
The ServiceNow MCP server provides the following change management tools:
### Core Change Request Management
1. **create_change_request** - Create a new change request in ServiceNow
- Parameters:
- `short_description` (required): Short description of the change request
- `description`: Detailed description of the change request
- `type` (required): Type of change (normal, standard, emergency)
- `risk`: Risk level of the change
- `impact`: Impact of the change
- `category`: Category of the change
- `requested_by`: User who requested the change
- `assignment_group`: Group assigned to the change
- `start_date`: Planned start date (YYYY-MM-DD HH:MM:SS)
- `end_date`: Planned end date (YYYY-MM-DD HH:MM:SS)
2. **update_change_request** - Update an existing change request
- Parameters:
- `change_id` (required): Change request ID or sys_id
- `short_description`: Short description of the change request
- `description`: Detailed description of the change request
- `state`: State of the change request
- `risk`: Risk level of the change
- `impact`: Impact of the change
- `category`: Category of the change
- `assignment_group`: Group assigned to the change
- `start_date`: Planned start date (YYYY-MM-DD HH:MM:SS)
- `end_date`: Planned end date (YYYY-MM-DD HH:MM:SS)
- `work_notes`: Work notes to add to the change request
3. **list_change_requests** - List change requests with filtering options
- Parameters:
- `limit`: Maximum number of records to return (default: 10)
- `offset`: Offset to start from (default: 0)
- `state`: Filter by state
- `type`: Filter by type (normal, standard, emergency)
- `category`: Filter by category
- `assignment_group`: Filter by assignment group
- `timeframe`: Filter by timeframe (upcoming, in-progress, completed)
- `query`: Additional query string
4. **get_change_request_details** - Get detailed information about a specific change request
- Parameters:
- `change_id` (required): Change request ID or sys_id
5. **add_change_task** - Add a task to a change request
- Parameters:
- `change_id` (required): Change request ID or sys_id
- `short_description` (required): Short description of the task
- `description`: Detailed description of the task
- `assigned_to`: User assigned to the task
- `planned_start_date`: Planned start date (YYYY-MM-DD HH:MM:SS)
- `planned_end_date`: Planned end date (YYYY-MM-DD HH:MM:SS)
### Change Approval Workflow
1. **submit_change_for_approval** - Submit a change request for approval
- Parameters:
- `change_id` (required): Change request ID or sys_id
- `approval_comments`: Comments for the approval request
2. **approve_change** - Approve a change request
- Parameters:
- `change_id` (required): Change request ID or sys_id
- `approver_id`: ID of the approver
- `approval_comments`: Comments for the approval
3. **reject_change** - Reject a change request
- Parameters:
- `change_id` (required): Change request ID or sys_id
- `approver_id`: ID of the approver
- `rejection_reason` (required): Reason for rejection
## Example Usage with Claude
Once the ServiceNow MCP server is configured with Claude Desktop, you can ask Claude to perform actions like:
### Creating and Managing Change Requests
- "Create a change request for server maintenance to apply security patches tomorrow night"
- "Schedule a database upgrade for next Tuesday from 2 AM to 4 AM"
- "Create an emergency change to fix the critical security vulnerability in our web application"
### Adding Tasks and Implementation Details
- "Add a task to the server maintenance change for pre-implementation checks"
- "Add a task to verify system backups before starting the database upgrade"
- "Update the implementation plan for the network change to include rollback procedures"
### Approval Workflow
- "Submit the server maintenance change for approval"
- "Show me all changes waiting for my approval"
- "Approve the database upgrade change with comment: implementation plan looks thorough"
- "Reject the network change due to insufficient testing"
### Querying Change Information
- "Show me all emergency changes scheduled for this week"
- "What's the status of the database upgrade change?"
- "List all changes assigned to the Network team"
- "Show me the details of change CHG0010001"
## Example Code
Here's an example of how to use the change management tools programmatically:
```python
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.tools.change_tools import create_change_request
from servicenow_mcp.utils.config import ServerConfig
# Create server configuration
server_config = ServerConfig(
instance_url="https://your-instance.service-now.com",
)
# Create authentication manager
auth_manager = AuthManager(
auth_type="basic",
username="your-username",
password="your-password",
instance_url="https://your-instance.service-now.com",
)
# Create a change request
create_params = {
"short_description": "Server maintenance - Apply security patches",
"description": "Apply the latest security patches to the application servers.",
"type": "normal",
"risk": "moderate",
"impact": "medium",
"category": "Hardware",
"start_date": "2023-12-15 01:00:00",
"end_date": "2023-12-15 03:00:00",
}
result = create_change_request(auth_manager, server_config, create_params)
print(result)
```
For a complete example, see the [change_management_demo.py](../examples/change_management_demo.py) script.
## Integration with Claude Desktop
To configure the ServiceNow MCP server with change management tools in Claude Desktop:
1. Edit the Claude Desktop configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the appropriate path for your OS:
```json
{
"mcpServers": {
"ServiceNow": {
"command": "/Users/yourusername/dev/servicenow-mcp/.venv/bin/python",
"args": [
"-m",
"servicenow_mcp.cli"
],
"env": {
"SERVICENOW_INSTANCE_URL": "https://your-instance.service-now.com",
"SERVICENOW_USERNAME": "your-username",
"SERVICENOW_PASSWORD": "your-password",
"SERVICENOW_AUTH_TYPE": "basic"
}
}
}
}
```
2. Restart Claude Desktop to apply the changes
## Customization
The change management tools can be customized to match your organization's specific ServiceNow configuration:
- State values may need to be adjusted based on your ServiceNow instance configuration
- Additional fields can be added to the parameter models if needed
- Approval workflows may need to be modified to match your organization's approval process
To customize the tools, modify the `change_tools.py` file in the `src/servicenow_mcp/tools` directory.
```
--------------------------------------------------------------------------------
/docs/knowledge_base.md:
--------------------------------------------------------------------------------
```markdown
# Knowledge Base Management
The ServiceNow MCP server provides tools for managing knowledge bases, categories, and articles in ServiceNow.
## Overview
Knowledge bases in ServiceNow allow organizations to store and share information in a structured format. This can include documentation, FAQs, troubleshooting guides, and other knowledge resources.
The ServiceNow MCP Knowledge Base tools provide a way to create and manage knowledge bases, categories, and articles through the ServiceNow API.
## Available Tools
### Knowledge Base Management
1. **create_knowledge_base** - Create a new knowledge base in ServiceNow
- Parameters:
- `title` - Title of the knowledge base
- `description` (optional) - Description of the knowledge base
- `owner` (optional) - The specified admin user or group
- `managers` (optional) - Users who can manage this knowledge base
- `publish_workflow` (optional) - Publication workflow
- `retire_workflow` (optional) - Retirement workflow
2. **list_knowledge_bases** - List knowledge bases from ServiceNow
- Parameters:
- `limit` (optional, default: 10) - Maximum number of knowledge bases to return
- `offset` (optional, default: 0) - Offset for pagination
- `active` (optional) - Filter by active status
- `query` (optional) - Search query for knowledge bases
### Category Management
1. **create_category** - Create a new category in a knowledge base
- Parameters:
- `title` - Title of the category
- `description` (optional) - Description of the category
- `knowledge_base` - The knowledge base to create the category in
- `parent_category` (optional) - Parent category (if creating a subcategory)
- `active` (optional, default: true) - Whether the category is active
2. **list_categories** - List categories in a knowledge base
- Parameters:
- `knowledge_base` (optional) - Filter by knowledge base ID
- `parent_category` (optional) - Filter by parent category ID
- `limit` (optional, default: 10) - Maximum number of categories to return
- `offset` (optional, default: 0) - Offset for pagination
- `active` (optional) - Filter by active status
- `query` (optional) - Search query for categories
### Article Management
1. **create_article** - Create a new knowledge article
- Parameters:
- `title` - Title of the article
- `text` - The main body text for the article
- `short_description` - Short description of the article
- `knowledge_base` - The knowledge base to create the article in
- `category` - Category for the article
- `keywords` (optional) - Keywords for search
- `article_type` (optional, default: "text") - The type of article
2. **update_article** - Update an existing knowledge article
- Parameters:
- `article_id` - ID of the article to update
- `title` (optional) - Updated title of the article
- `text` (optional) - Updated main body text for the article
- `short_description` (optional) - Updated short description
- `category` (optional) - Updated category for the article
- `keywords` (optional) - Updated keywords for search
3. **publish_article** - Publish a knowledge article
- Parameters:
- `article_id` - ID of the article to publish
- `workflow_state` (optional, default: "published") - The workflow state to set
- `workflow_version` (optional) - The workflow version to use
4. **list_articles** - List knowledge articles with filtering options
- Parameters:
- `limit` (optional, default: 10) - Maximum number of articles to return
- `offset` (optional, default: 0) - Offset for pagination
- `knowledge_base` (optional) - Filter by knowledge base
- `category` (optional) - Filter by category
- `query` (optional) - Search query for articles
- `workflow_state` (optional) - Filter by workflow state
5. **get_article** - Get a specific knowledge article by ID
- Parameters:
- `article_id` - ID of the article to get
## Example Usage
### Creating a Knowledge Base
```python
response = create_knowledge_base({
"title": "Healthcare IT Knowledge Base",
"description": "Knowledge base for healthcare IT resources and documentation",
"owner": "healthcare_admins"
})
print(f"Knowledge base created with ID: {response['kb_id']}")
```
### Listing Knowledge Bases
```python
response = list_knowledge_bases({
"active": True,
"query": "IT",
"limit": 20
})
print(f"Found {response['count']} knowledge bases")
for kb in response['knowledge_bases']:
print(f"- {kb['title']}")
```
### Creating a Category
```python
response = create_category({
"title": "Network Configuration",
"description": "Articles related to network configuration in healthcare environments",
"knowledge_base": "healthcare_kb"
})
print(f"Category created with ID: {response['category_id']}")
```
### Creating an Article
```python
response = create_article({
"title": "VPN Setup for Remote Clinicians",
"short_description": "Step-by-step guide for setting up VPN access for remote clinicians",
"text": "# VPN Setup Guide\n\n1. Install the VPN client...",
"knowledge_base": "healthcare_kb",
"category": "network_config",
"keywords": "vpn, remote access, clinicians, setup"
})
print(f"Article created with ID: {response['article_id']}")
```
### Updating an Article
```python
response = update_article({
"article_id": "kb0010001",
"text": "# Updated VPN Setup Guide\n\n1. Download the latest VPN client...",
"keywords": "vpn, remote access, clinicians, setup, configuration"
})
print(f"Article updated: {response['success']}")
```
### Publishing an Article
```python
response = publish_article({
"article_id": "kb0010001"
})
print(f"Article published: {response['success']}")
```
### Listing Articles
```python
response = list_articles({
"knowledge_base": "healthcare_kb",
"category": "network_config",
"limit": 20
})
print(f"Found {response['count']} articles")
for article in response['articles']:
print(f"- {article['title']}")
```
### Getting Article Details
```python
response = get_article({
"article_id": "kb0010001"
})
article = response['article']
print(f"Title: {article['title']}")
print(f"Category: {article['category']}")
print(f"Views: {article['views']}")
```
### Listing Categories
```python
response = list_categories({
"knowledge_base": "healthcare_kb",
"active": True,
"limit": 20
})
print(f"Found {response['count']} categories")
for category in response['categories']:
print(f"- {category['title']}")
```
## ServiceNow API Endpoints
The Knowledge Base tools use the following ServiceNow API endpoints:
- `/table/kb_knowledge_base` - Knowledge base table API
- `/table/kb_category` - Category table API
- `/table/kb_knowledge` - Knowledge article table API
## Error Handling
All knowledge base tools handle errors gracefully and return responses with `success` and `message` fields. If an operation fails, the `success` field will be `false` and the `message` field will contain information about the error.
## Additional Information
For more information about knowledge management in ServiceNow, see the [ServiceNow Knowledge Management documentation](https://docs.servicenow.com/bundle/tokyo-servicenow-platform/page/product/knowledge-management/concept/c_KnowledgeManagement.html).
```
--------------------------------------------------------------------------------
/prompts/add_servicenow_mcp_tool.md:
--------------------------------------------------------------------------------
```markdown
# ServiceNow MCP Tool Creation Prompt
Use this prompt to guide the development of new tools for the ServiceNow MCP project. This structured approach ensures consistency in implementation, testing, and documentation.
## Background
The ServiceNow MCP (Model Completion Protocol) server allows Claude to interact with ServiceNow instances, retrieving data and performing actions through the ServiceNow API. Adding new tools expands the capabilities of this bridge.
## Required Files to Create/Modify
For each new tool, you need to:
1. Create/modify a tool module in `src/servicenow_mcp/tools/`
2. Update the tools `__init__.py` to expose the new tool
3. Update `server.py` to register the tool with the MCP server
4. Add unit tests in the `tests/` directory
5. Update documentation in the `docs/` directory
6. Update the `README.md` to include the new tool
## Implementation Steps
Please implement the following ServiceNow tool capability: {DESCRIBE_CAPABILITY_HERE}
Follow these steps to ensure a complete implementation:
### 1. Tool Module Implementation
```python
# Create a new file or modify an existing module in src/servicenow_mcp/tools/
"""
{TOOL_NAME} tools for the ServiceNow MCP server.
This module provides tools for {TOOL_DESCRIPTION}.
"""
import logging
from typing import Optional, List
import requests
from pydantic import BaseModel, Field
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.utils.config import ServerConfig
logger = logging.getLogger(__name__)
class {ToolName}Params(BaseModel):
"""Parameters for {tool_name}."""
# Define parameters with type annotations and descriptions
param1: str = Field(..., description="Description of parameter 1")
param2: Optional[str] = Field(None, description="Description of parameter 2")
# Add more parameters as needed
class {ToolName}Response(BaseModel):
"""Response from {tool_name} operations."""
success: bool = Field(..., description="Whether the operation was successful")
message: str = Field(..., description="Message describing the result")
# Add more response fields as needed
def {tool_name}(
config: ServerConfig,
auth_manager: AuthManager,
params: {ToolName}Params,
) -> {ToolName}Response:
"""
{Tool description with detailed explanation}.
Args:
config: Server configuration.
auth_manager: Authentication manager.
params: Parameters for {tool_name}.
Returns:
Response with {description of response}.
"""
api_url = f"{config.api_url}/table/{table_name}"
# Build request data
data = {
# Map parameters to API request fields
"field1": params.param1,
}
if params.param2:
data["field2"] = params.param2
# Add conditional fields as needed
# Make request
try:
response = requests.post( # or get, patch, delete as appropriate
api_url,
json=data,
headers=auth_manager.get_headers(),
timeout=config.timeout,
)
response.raise_for_status()
result = response.json().get("result", {})
return {ToolName}Response(
success=True,
message="{Tool name} completed successfully",
# Add more response fields from result as needed
)
except requests.RequestException as e:
logger.error(f"Failed to {tool_name}: {e}")
return {ToolName}Response(
success=False,
message=f"Failed to {tool_name}: {str(e)}",
)
```
### 2. Update tools/__init__.py
```python
# Add import for new tool
from servicenow_mcp.tools.{tool_module} import (
{tool_name},
)
# Add to __all__ list
__all__ = [
# Existing tools...
# New tools
"{tool_name}",
]
```
### 3. Update server.py
```python
# Add imports for the tool parameters and function
from servicenow_mcp.tools.{tool_module} import (
{ToolName}Params,
)
from servicenow_mcp.tools.{tool_module} import (
{tool_name} as {tool_name}_tool,
)
# In the _register_tools method, add:
@self.mcp_server.tool()
def {tool_name}(params: {ToolName}Params) -> Dict[str, Any]:
return {tool_name}_tool(
self.config,
self.auth_manager,
params,
)
```
### 4. Add Unit Tests
```python
# Add to existing test file or create a new one in tests/test_{tool_module}.py
@patch("requests.post") # Or appropriate HTTP method
def test_{tool_name}(self, mock_post):
"""Test {tool_name} function."""
# Configure mock
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_response.json.return_value = {
"result": {
# Mocked response data
}
}
mock_post.return_value = mock_response
# Create test params
params = {ToolName}Params(
param1="value1",
param2="value2",
)
# Call function
result = {tool_name}(self.config, self.auth_manager, params)
# Verify result
self.assertTrue(result.success)
# Add more assertions
# Verify mock was called correctly
mock_post.assert_called_once()
call_args = mock_post.call_args
self.assertEqual(call_args[0][0], f"{self.config.api_url}/table/{table_name}")
# Add more assertions for request data
```
### 5. Update Documentation
Create or update a markdown file in `docs/` that explains the tool:
```markdown
# {Tool Category} in ServiceNow MCP
## {Tool Name}
{Detailed description of the tool}
### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| param1 | string | Yes | Description of parameter 1 |
| param2 | string | No | Description of parameter 2 |
| ... | ... | ... | ... |
### Example
```python
# Example usage of {tool_name}
result = {tool_name}({
"param1": "value1",
"param2": "value2"
})
```
### Response
The tool returns a response with the following fields:
| Field | Type | Description |
|-------|------|-------------|
| success | boolean | Whether the operation was successful |
| message | string | A message describing the result |
| ... | ... | ... |
```
### 6. Update README.md
Add the new tool to the appropriate section in README.md:
```markdown
#### {Tool Category} Tools
1. **existing_tool** - Description of existing tool
...
N. **{tool_name}** - {Brief description of the new tool}
```
Also add example usage to the "Example Usage with Claude" section:
```markdown
#### {Tool Category} Examples
- "Existing example query"
...
- "{Example natural language query that would use the new tool}"
```
## Testing Guidelines
1. Write unit tests for all new functions and edge cases
2. Mock all external API calls
3. Test failure conditions and error handling
4. Verify parameter validation
## Documentation Guidelines
1. Use clear, concise language
2. Include all parameters and their descriptions
3. Provide usage examples
4. Document common errors and troubleshooting steps
5. Update README.md to showcase the new functionality
## Best Practices
1. Follow existing code patterns and style
2. Add appropriate error handling
3. Include detailed logging
4. Use meaningful variable and function names
5. Add type hints and docstrings
6. Keep functions focused and single-purpose
## Example Natural Language Commands for Claude
List examples of natural language prompts that users can give to Claude that would trigger the new tool:
- "Prompt example 1"
- "Prompt example 2"
- "Prompt example 3"
```
--------------------------------------------------------------------------------
/docs/catalog.md:
--------------------------------------------------------------------------------
```markdown
# ServiceNow Service Catalog Integration
This document provides information about the ServiceNow Service Catalog integration in the ServiceNow MCP server.
## Overview
The ServiceNow Service Catalog integration allows you to:
- List service catalog categories
- List service catalog items
- Get detailed information about specific catalog items, including their variables
- Filter catalog items by category or search query
## Tools
The following tools are available for interacting with the ServiceNow Service Catalog:
### `list_catalog_categories`
Lists available service catalog categories.
**Parameters:**
- `limit` (int, default: 10): Maximum number of categories to return
- `offset` (int, default: 0): Offset for pagination
- `query` (string, optional): Search query for categories
- `active` (boolean, default: true): Whether to only return active categories
**Example:**
```python
from servicenow_mcp.tools.catalog_tools import ListCatalogCategoriesParams, list_catalog_categories
params = ListCatalogCategoriesParams(
limit=5,
query="hardware"
)
result = list_catalog_categories(config, auth_manager, params)
```
### `create_catalog_category`
Creates a new service catalog category.
**Parameters:**
- `title` (string, required): Title of the category
- `description` (string, optional): Description of the category
- `parent` (string, optional): Parent category sys_id
- `icon` (string, optional): Icon for the category
- `active` (boolean, default: true): Whether the category is active
- `order` (integer, optional): Order of the category
**Example:**
```python
from servicenow_mcp.tools.catalog_tools import CreateCatalogCategoryParams, create_catalog_category
params = CreateCatalogCategoryParams(
title="Cloud Services",
description="Cloud-based services and resources",
parent="parent_category_id",
icon="cloud"
)
result = create_catalog_category(config, auth_manager, params)
```
### `update_catalog_category`
Updates an existing service catalog category.
**Parameters:**
- `category_id` (string, required): Category ID or sys_id
- `title` (string, optional): Title of the category
- `description` (string, optional): Description of the category
- `parent` (string, optional): Parent category sys_id
- `icon` (string, optional): Icon for the category
- `active` (boolean, optional): Whether the category is active
- `order` (integer, optional): Order of the category
**Example:**
```python
from servicenow_mcp.tools.catalog_tools import UpdateCatalogCategoryParams, update_catalog_category
params = UpdateCatalogCategoryParams(
category_id="category123",
title="IT Equipment",
description="Updated description for IT equipment"
)
result = update_catalog_category(config, auth_manager, params)
```
### `move_catalog_items`
Moves catalog items to a different category.
**Parameters:**
- `item_ids` (list of strings, required): List of catalog item IDs to move
- `target_category_id` (string, required): Target category ID to move items to
**Example:**
```python
from servicenow_mcp.tools.catalog_tools import MoveCatalogItemsParams, move_catalog_items
params = MoveCatalogItemsParams(
item_ids=["item1", "item2", "item3"],
target_category_id="target_category_id"
)
result = move_catalog_items(config, auth_manager, params)
```
### `list_catalog_items`
Lists available service catalog items.
**Parameters:**
- `limit` (int, default: 10): Maximum number of items to return
- `offset` (int, default: 0): Offset for pagination
- `category` (string, optional): Filter by category
- `query` (string, optional): Search query for items
- `active` (boolean, default: true): Whether to only return active items
**Example:**
```python
from servicenow_mcp.tools.catalog_tools import ListCatalogItemsParams, list_catalog_items
params = ListCatalogItemsParams(
limit=5,
category="hardware",
query="laptop"
)
result = list_catalog_items(config, auth_manager, params)
```
### `get_catalog_item`
Gets detailed information about a specific catalog item.
**Parameters:**
- `item_id` (string, required): Catalog item ID or sys_id
**Example:**
```python
from servicenow_mcp.tools.catalog_tools import GetCatalogItemParams, get_catalog_item
params = GetCatalogItemParams(
item_id="item123"
)
result = get_catalog_item(config, auth_manager, params)
```
## Resources
The following resources are available for accessing the ServiceNow Service Catalog:
### `catalog://items`
Lists service catalog items.
**Example:**
```
catalog://items
```
### `catalog://categories`
Lists service catalog categories.
**Example:**
```
catalog://categories
```
### `catalog://{item_id}`
Gets a specific catalog item by ID.
**Example:**
```
catalog://item123
```
## Integration with Claude Desktop
To use the ServiceNow Service Catalog with Claude Desktop:
1. Configure the ServiceNow MCP server in Claude Desktop
2. Ask Claude questions about the service catalog
**Example prompts:**
- "Can you list the available service catalog categories in ServiceNow?"
- "Can you show me the available items in the ServiceNow service catalog?"
- "Can you list the catalog items in the Hardware category?"
- "Can you show me the details of the 'New Laptop' catalog item?"
- "Can you find catalog items related to 'software' in ServiceNow?"
- "Can you create a new category called 'Cloud Services' in the service catalog?"
- "Can you update the 'Hardware' category to rename it to 'IT Equipment'?"
- "Can you move the 'Virtual Machine' catalog item to the 'Cloud Services' category?"
- "Can you create a subcategory called 'Monitors' under the 'IT Equipment' category?"
- "Can you reorganize our catalog by moving all software items to the 'Software' category?"
## Example Scripts
### Integration Test
The `examples/catalog_integration_test.py` script demonstrates how to use the catalog tools directly:
```bash
python examples/catalog_integration_test.py
```
### Claude Desktop Demo
The `examples/claude_catalog_demo.py` script demonstrates how to use the catalog functionality with Claude Desktop:
```bash
python examples/claude_catalog_demo.py
```
## Data Models
### CatalogItemModel
Represents a ServiceNow catalog item.
**Fields:**
- `sys_id` (string): Unique identifier for the catalog item
- `name` (string): Name of the catalog item
- `short_description` (string, optional): Short description of the catalog item
- `description` (string, optional): Detailed description of the catalog item
- `category` (string, optional): Category of the catalog item
- `price` (string, optional): Price of the catalog item
- `picture` (string, optional): Picture URL of the catalog item
- `active` (boolean, optional): Whether the catalog item is active
- `order` (integer, optional): Order of the catalog item in its category
### CatalogCategoryModel
Represents a ServiceNow catalog category.
**Fields:**
- `sys_id` (string): Unique identifier for the category
- `title` (string): Title of the category
- `description` (string, optional): Description of the category
- `parent` (string, optional): Parent category ID
- `icon` (string, optional): Icon of the category
- `active` (boolean, optional): Whether the category is active
- `order` (integer, optional): Order of the category
### CatalogItemVariableModel
Represents a ServiceNow catalog item variable.
**Fields:**
- `sys_id` (string): Unique identifier for the variable
- `name` (string): Name of the variable
- `label` (string): Label of the variable
- `type` (string): Type of the variable
- `mandatory` (boolean, optional): Whether the variable is mandatory
- `default_value` (string, optional): Default value of the variable
- `help_text` (string, optional): Help text for the variable
- `order` (integer, optional): Order of the variable
```
--------------------------------------------------------------------------------
/tests/test_changeset_resources.py:
--------------------------------------------------------------------------------
```python
"""
Tests for the changeset resources.
This module contains tests for the changeset resources in the ServiceNow MCP server.
"""
import json
import unittest
import requests
from unittest.mock import MagicMock, patch
from servicenow_mcp.auth.auth_manager import AuthManager
from servicenow_mcp.resources.changesets import ChangesetListParams, ChangesetResource
from servicenow_mcp.utils.config import ServerConfig, AuthConfig, AuthType, BasicAuthConfig
class TestChangesetResource(unittest.IsolatedAsyncioTestCase):
"""Tests for the changeset resource."""
def setUp(self):
"""Set up test fixtures."""
auth_config = AuthConfig(
type=AuthType.BASIC,
basic=BasicAuthConfig(
username="test_user",
password="test_password"
)
)
self.server_config = ServerConfig(
instance_url="https://test.service-now.com",
auth=auth_config,
)
self.auth_manager = MagicMock(spec=AuthManager)
self.auth_manager.get_headers.return_value = {"Authorization": "Bearer test"}
self.changeset_resource = ChangesetResource(self.server_config, self.auth_manager)
@patch("servicenow_mcp.resources.changesets.requests.get")
async def test_list_changesets(self, mock_get):
"""Test listing changesets."""
# Mock response
mock_response = MagicMock()
mock_response.text = json.dumps({
"result": [
{
"sys_id": "123",
"name": "Test Changeset",
"state": "in_progress",
"application": "Test App",
"developer": "test.user",
}
]
})
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
# Call the function
params = ChangesetListParams(
limit=10,
offset=0,
state="in_progress",
application="Test App",
developer="test.user",
)
result = await self.changeset_resource.list_changesets(params)
# Verify the result
result_json = json.loads(result)
self.assertEqual(len(result_json["result"]), 1)
self.assertEqual(result_json["result"][0]["sys_id"], "123")
self.assertEqual(result_json["result"][0]["name"], "Test Changeset")
# Verify the API call
mock_get.assert_called_once()
args, kwargs = mock_get.call_args
self.assertEqual(args[0], "https://test.service-now.com/api/now/table/sys_update_set")
self.assertEqual(kwargs["headers"], {"Authorization": "Bearer test"})
self.assertEqual(kwargs["params"]["sysparm_limit"], 10)
self.assertEqual(kwargs["params"]["sysparm_offset"], 0)
self.assertIn("sysparm_query", kwargs["params"])
self.assertIn("state=in_progress", kwargs["params"]["sysparm_query"])
self.assertIn("application=Test App", kwargs["params"]["sysparm_query"])
self.assertIn("developer=test.user", kwargs["params"]["sysparm_query"])
@patch("servicenow_mcp.resources.changesets.requests.get")
async def test_get_changeset(self, mock_get):
"""Test getting a changeset."""
# Mock responses
mock_changeset_response = MagicMock()
mock_changeset_response.json.return_value = {
"result": {
"sys_id": "123",
"name": "Test Changeset",
"state": "in_progress",
"application": "Test App",
"developer": "test.user",
}
}
mock_changeset_response.raise_for_status.return_value = None
mock_changes_response = MagicMock()
mock_changes_response.json.return_value = {
"result": [
{
"sys_id": "456",
"name": "test_file.py",
"type": "file",
"update_set": "123",
}
]
}
mock_changes_response.raise_for_status.return_value = None
# Set up the mock to return different responses for different URLs
def side_effect(*args, **kwargs):
url = args[0]
if "sys_update_set" in url:
return mock_changeset_response
elif "sys_update_xml" in url:
return mock_changes_response
return None
mock_get.side_effect = side_effect
# Call the function
result = await self.changeset_resource.get_changeset("123")
# Verify the result
result_json = json.loads(result)
self.assertEqual(result_json["changeset"]["sys_id"], "123")
self.assertEqual(result_json["changeset"]["name"], "Test Changeset")
self.assertEqual(len(result_json["changes"]), 1)
self.assertEqual(result_json["changes"][0]["sys_id"], "456")
self.assertEqual(result_json["changes"][0]["name"], "test_file.py")
self.assertEqual(result_json["change_count"], 1)
# Verify the API calls
self.assertEqual(mock_get.call_count, 2)
first_call_args, first_call_kwargs = mock_get.call_args_list[0]
self.assertEqual(
first_call_args[0], "https://test.service-now.com/api/now/table/sys_update_set/123"
)
self.assertEqual(first_call_kwargs["headers"], {"Authorization": "Bearer test"})
second_call_args, second_call_kwargs = mock_get.call_args_list[1]
self.assertEqual(
second_call_args[0], "https://test.service-now.com/api/now/table/sys_update_xml"
)
self.assertEqual(second_call_kwargs["headers"], {"Authorization": "Bearer test"})
self.assertEqual(second_call_kwargs["params"]["sysparm_query"], "update_set=123")
@patch("servicenow_mcp.resources.changesets.requests.get")
async def test_list_changesets_error(self, mock_get):
"""Test listing changesets with an error."""
# Mock response
mock_get.side_effect = requests.exceptions.RequestException("Test error")
# Call the function
params = ChangesetListParams()
result = await self.changeset_resource.list_changesets(params)
# Verify the result
result_json = json.loads(result)
self.assertIn("error", result_json)
self.assertIn("Test error", result_json["error"])
@patch("servicenow_mcp.resources.changesets.requests.get")
async def test_get_changeset_error(self, mock_get):
"""Test getting a changeset with an error."""
# Mock response
mock_get.side_effect = requests.exceptions.RequestException("Test error")
# Call the function
result = await self.changeset_resource.get_changeset("123")
# Verify the result
result_json = json.loads(result)
self.assertIn("error", result_json)
self.assertIn("Test error", result_json["error"])
class TestChangesetListParams(unittest.TestCase):
"""Tests for the ChangesetListParams class."""
def test_changeset_list_params(self):
"""Test ChangesetListParams."""
params = ChangesetListParams(
limit=20,
offset=10,
state="in_progress",
application="Test App",
developer="test.user",
)
self.assertEqual(params.limit, 20)
self.assertEqual(params.offset, 10)
self.assertEqual(params.state, "in_progress")
self.assertEqual(params.application, "Test App")
self.assertEqual(params.developer, "test.user")
def test_changeset_list_params_defaults(self):
"""Test ChangesetListParams defaults."""
params = ChangesetListParams()
self.assertEqual(params.limit, 10)
self.assertEqual(params.offset, 0)
self.assertIsNone(params.state)
self.assertIsNone(params.application)
self.assertIsNone(params.developer)
if __name__ == "__main__":
unittest.main()
```
--------------------------------------------------------------------------------
/examples/catalog_optimization_example.py:
--------------------------------------------------------------------------------
```python
#!/usr/bin/env python3
"""
Example script demonstrating how to use the ServiceNow MCP catalog optimization tools.
This script shows how to:
1. Get optimization recommendations for a ServiceNow Service Catalog
2. Update catalog items with improved descriptions
Usage:
python catalog_optimization_example.py [--update-descriptions]
Options:
--update-descriptions Automatically update items with poor descriptions
"""
import argparse
import logging
import sys
from typing import Dict
from servicenow_mcp.server import ServiceNowMCP
from servicenow_mcp.tools.catalog_optimization import (
OptimizationRecommendationsParams,
UpdateCatalogItemParams,
)
from servicenow_mcp.utils.config import load_config
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
def get_optimization_recommendations(server: ServiceNowMCP) -> Dict:
"""
Get optimization recommendations for the ServiceNow Service Catalog.
Args:
server: The ServiceNowMCP server instance
Returns:
Dict containing the optimization recommendations
"""
logger.info("Getting catalog optimization recommendations...")
# Create parameters for all recommendation types
params = OptimizationRecommendationsParams(
recommendation_types=[
"inactive_items",
"low_usage",
"high_abandonment",
"slow_fulfillment",
"description_quality",
]
)
# Call the tool
result = server.tools["get_optimization_recommendations"](params)
if not result["success"]:
logger.error(f"Failed to get optimization recommendations: {result.get('message', 'Unknown error')}")
return {}
return result
def print_recommendations(recommendations: Dict) -> None:
"""
Print the optimization recommendations in a readable format.
Args:
recommendations: The optimization recommendations dictionary
"""
if not recommendations or "recommendations" not in recommendations:
logger.warning("No recommendations available")
return
print("\n" + "=" * 80)
print("SERVICENOW CATALOG OPTIMIZATION RECOMMENDATIONS")
print("=" * 80)
for rec in recommendations["recommendations"]:
print(f"\n{rec['title']} ({rec['type']})")
print("-" * len(rec['title']))
print(f"Description: {rec['description']}")
print(f"Impact: {rec['impact'].upper()}")
print(f"Effort: {rec['effort'].upper()}")
print(f"Recommended Action: {rec['action']}")
if rec["items"]:
print("\nAffected Items:")
for i, item in enumerate(rec["items"], 1):
print(f" {i}. {item['name']}")
print(f" ID: {item['sys_id']}")
print(f" Description: {item['short_description'] or '(No description)'}")
# Print additional details based on recommendation type
if rec["type"] == "low_usage":
print(f" Order Count: {item['order_count']}")
elif rec["type"] == "high_abandonment":
print(f" Abandonment Rate: {item['abandonment_rate']}%")
print(f" Cart Adds: {item['cart_adds']}")
print(f" Completed Orders: {item['orders']}")
elif rec["type"] == "slow_fulfillment":
print(f" Avg. Fulfillment Time: {item['avg_fulfillment_time']} days")
print(f" Compared to Catalog Avg: {item['avg_fulfillment_time_vs_catalog']}x slower")
elif rec["type"] == "description_quality":
print(f" Description Quality Score: {item['description_quality']}/100")
print(f" Issues: {', '.join(item['quality_issues'])}")
print()
else:
print("\nNo items found in this category.")
print("=" * 80)
def update_poor_descriptions(server: ServiceNowMCP, recommendations: Dict) -> None:
"""
Update catalog items with poor descriptions.
Args:
server: The ServiceNowMCP server instance
recommendations: The optimization recommendations dictionary
"""
# Find the description quality recommendation
description_rec = None
for rec in recommendations.get("recommendations", []):
if rec["type"] == "description_quality":
description_rec = rec
break
if not description_rec or not description_rec.get("items"):
logger.warning("No items with poor descriptions found")
return
logger.info(f"Found {len(description_rec['items'])} items with poor descriptions")
# Update each item with a better description
for item in description_rec["items"]:
# Generate an improved description based on the item name and category
improved_description = generate_improved_description(item)
logger.info(f"Updating description for item: {item['name']} (ID: {item['sys_id']})")
logger.info(f" Original: {item['short_description'] or '(No description)'}")
logger.info(f" Improved: {improved_description}")
# Create parameters for updating the item
params = UpdateCatalogItemParams(
item_id=item["sys_id"],
short_description=improved_description,
)
# Call the tool
result = server.tools["update_catalog_item"](params)
if result["success"]:
logger.info(f"Successfully updated description for {item['name']}")
else:
logger.error(f"Failed to update description: {result.get('message', 'Unknown error')}")
def generate_improved_description(item: Dict) -> str:
"""
Generate an improved description for a catalog item.
In a real implementation, this could use AI to generate better descriptions,
but for this example we'll use a simple template-based approach.
Args:
item: The catalog item dictionary
Returns:
An improved description string
"""
name = item["name"]
category = item.get("category", "").lower()
# Simple templates based on category
if "hardware" in category:
return f"Enterprise-grade {name.lower()} for professional use. Includes standard warranty and IT support."
elif "software" in category:
return f"Licensed {name.lower()} application with full feature set. Includes installation support."
elif "service" in category:
return f"Professional {name.lower()} service delivered by our expert team. Includes consultation and implementation."
else:
return f"High-quality {name.lower()} available through IT self-service. Contact the service desk for assistance."
def main():
"""Main function to run the example."""
parser = argparse.ArgumentParser(description="ServiceNow Catalog Optimization Example")
parser.add_argument(
"--update-descriptions",
action="store_true",
help="Automatically update items with poor descriptions",
)
args = parser.parse_args()
try:
# Load configuration
config = load_config()
# Initialize the ServiceNow MCP server
server = ServiceNowMCP(config)
# Get optimization recommendations
recommendations = get_optimization_recommendations(server)
# Print the recommendations
print_recommendations(recommendations)
# Update poor descriptions if requested
if args.update_descriptions and recommendations:
update_poor_descriptions(server, recommendations)
except Exception as e:
logger.exception(f"Error running catalog optimization example: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
```