-
-
Notifications
You must be signed in to change notification settings - Fork 1
Development
This guide is for contributors working on the MCP server itself - not for consumers integrating it into an AI client.
Related pages: Architecture, Telemetry.
The project uses a src-layout with hatchling as the build backend and uv as the package manager.
src/servicenow_mcp/ # Package root
server.py # Entry point (console script servicenow-platform-mcp)
client.py # ServiceNowClient - async HTTP
config.py # Settings (pydantic-settings)
packages.py # Package registry and tool group mapping
tools/ # One module per tool group
investigations/ # Investigation modules (7)
tests/ # Unit and integration tests
integration/ # Live-instance tests (require .env.local)
.github/workflows/ # CI, CodeQL, release-please, stale
pyproject.toml # Metadata, dependencies, tool configuration
Entry point: servicenow_mcp.server:main (registered as the servicenow-platform-mcp console script).
Build backend: hatchling. Package manager: uv (not pip, not poetry).
uv sync --group dev
cp .env.example .env.local # fill in ServiceNow credentialsNo build step is needed for development. Run the server with uv run servicenow-platform-mcp.
uv run pytest # all unit tests (integration excluded)
uv run pytest tests/test_client.py # single file
uv run pytest tests/test_client.py::TestClass::test_method # single test
uv run pytest -k "keyword" # keyword filter
uv run pytest -m integration # integration tests (needs .env.local)
uv run pytest --no-cov # skip coverage for speeduv run ruff check . # lint
uv run ruff check --fix . # auto-fix
uv run ruff format . # format
uv run ruff format --check . # verify without writinguv run mypy src/Configured in pyproject.toml. Key settings:
| Setting | Value |
|---|---|
| Line length | 120 |
| Quote style | double |
| Target | py312 |
| Src dirs | ["src", "tests"] |
Selected rule families:
E, F, W, I, UP, B, SIM, RUF, C4, DTZ, T20, PTH, TC, RET, PLW, PT, A, COM, PIE, ISC, G, INP, TID, ERA
Ignored rules:
E501, COM812, ISC001, TC001, TC002, TC003, RET504, RET505
Per-file ignores for tests/**/*.py: T20, ERA001, PT019.
isort is configured with known-first-party = ["servicenow_mcp"].
| Setting | Value |
|---|---|
python_version |
"3.12" |
disallow_untyped_defs |
true |
ignore_missing_imports |
true |
warn_return_any |
false |
warn_unused_configs |
true |
Module override: servicenow_mcp.server disables the call-arg error code. This accommodates FastMCP's dynamic tool registration signature.
| Setting | Value |
|---|---|
asyncio_mode |
"auto" |
testpaths |
["tests"] |
| Default addopts | -m 'not integration' --cov=servicenow_mcp --cov-report=xml --cov-report=term-missing |
The integration marker gates tests that run against a live ServiceNow instance. These require credentials in .env.local and are excluded by default.
Every tool group module exports a register_tools function with the canonical 5-argument signature. The server loader calls every group with identical arguments regardless of whether the group needs them all.
def register_tools(
mcp: FastMCP,
settings: Settings,
auth_provider: BasicAuthProvider,
choices: ChoiceRegistry | None = None,
dictionary: DictionaryRegistry | None = None,
) -> None:
del choices, dictionary # unused; signature retained for loader parity
@mcp.tool()
@tool_handler
async def my_tool(table: str, correlation_id: str = "") -> str:
"""Brief description shown to the AI client.
Args:
table: The ServiceNow table name.
"""
validate_identifier(table)
check_table_access(table)
async with ServiceNowClient(settings, auth_provider) as client:
result = await client.some_method(table)
return format_response(data=result, correlation_id=correlation_id)Rules:
- Decorator order is
@mcp.tool()then@tool_handler. -
correlation_id: str = ""is always the last parameter.@tool_handlerauto-injects it and hides it from the MCP schema. - Never raise to the caller.
@tool_handlerwraps every call insafe_tool_call()which catches exceptions and returns serialized error envelopes. - The docstring
Args:section becomes the tool's parameter descriptions in MCP. - Register your group in
_TOOL_GROUP_MODULESand add it to the appropriate preset packages inpackages.py.
The one exception: list_tool_packages is registered inline in server.py without @tool_handler. It has no correlation_id, no error envelope, and returns serialize(list_packages()) directly.
All unit tests mock HTTP via the respx library:
@respx.mock
async def test_query_success(self, settings, auth_provider):
respx.get("https://test.service-now.com/api/now/table/incident").mock(
return_value=httpx.Response(200, json={"result": [...]})
)
tools = _register_and_get_tools(settings, auth_provider)
raw = await tools["query"](table="incident", encoded_query="active=true")
result = json.loads(raw)
assert result["status"] == "success"Use the helper with the same 5-argument signature:
def _register_and_get_tools(settings, auth_provider, choices=None, dictionary=None):
mcp = FastMCP("test")
register_tools(mcp, settings, auth_provider, choices=choices, dictionary=dictionary)
return {t.name: t.fn for t in mcp._tool_manager._tools.values()}All tools return a serialized JSON string via format_response. Parse with json.loads:
raw = await tools["my_tool"](table="incident")
result = json.loads(raw)
assert result["status"] == "success"
assert result["correlation_id"] # always present- Located in
tests/integration/ - Marked with
@pytest.mark.integration - Excluded by default; run with
uv run pytest -m integration - Require
.env.localwith real ServiceNow credentials - Use session-scoped fixtures for the instance connection
Four workflows live in .github/workflows/:
Triggers on push to main and PRs targeting main. Concurrency groups cancel in-progress runs.
Three parallel jobs on ubuntu-latest:
-
lint -
uv run ruff check .anduv run ruff format --check . -
type-check -
uv run mypy src/ -
test - matrix over Python 3.12, 3.13, 3.14 (
fail-fast: false). Coverage uploaded to Codecov on 3.12 only.
GitHub CodeQL static analysis for Python. Runs on push, PRs, and a weekly cron (Monday 06:00 UTC).
Automated releases driven by conventional commits. See Releases below.
Marks inactive issues and PRs as stale (daily cron).
The project uses release-please (googleapis/release-please-action@v5) with conventional commits.
Workflow:
- Land changes on
mainusing conventional commit messages (feat:,fix:,docs:, etc.). - release-please opens or updates a release PR with a generated changelog.
- Merging that PR creates a GitHub Release and git tag.
- A conditional publish job runs
uv buildthenuv publishto PyPI.
Version is managed in pyproject.toml. release-please handles the bump automatically.
Required repository secrets: RELEASE_PLEASE_TOKEN, PYPI_TOKEN, CODECOV_TOKEN.