#
tokens: 48741/50000 45/79 files (page 1/4)
lines: off (toggle) GitHub
raw markdown copy
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
[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/osomai-servicenow-mcp-badge.png)](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() 
```
Page 1/4FirstPrevNextLast