mcaxl/pyproject.toml
Ryan Malloy 59f9df5b3b sql_validator: swap regex for sqlparse tokenization
The regex-based validator worked for everything tested, but had a
class of structural blindspot: it didn't actually know what a token
was, so it accepted `SELECT 1; SELECT 2` (no forbidden keyword in
either statement) and relied entirely on the keyword scan catching
write verbs. With sqlparse we get:

- Explicit multi-statement detection via `len(sqlparse.parse(query))`
  — `SELECT 1; SELECT 2` is now refused with a clear "Multiple
  statements detected" message.
- Proper string/comment boundary handling — `'log: DROP detected'`
  is one Literal.String token; the DROP inside it never reaches the
  forbidden-keyword scan. `inserted_at` is one Name token; INSERT
  isn't matched as a substring.
- Same conservative behavior for keywords-as-identifiers (sqlparse
  is a lexer, not a parser, so `SELECT delete FROM device` is still
  refused — CUCM's data dictionary doesn't use SQL keywords as
  column names anyway).

Hamilton review CRITICAL #1 preserved: the cleaned query returned to
the caller is still byte-for-byte the input (modulo trailing ; and
outer whitespace). sqlparse is consulted for analysis only.

Tests: +6 sqlparse-specific cases in TestSqlparseSpecific covering
multi-statement, comment-disguised injection, keyword-substring
identifiers, and CTE walks. 2 existing tests broadened from
match="DROP" to match="DROP|Multiple" — same query refused, the
diagnosis just got more accurate (multi-statement caught earlier
than forbidden-keyword scan).

36/36 validator tests pass.
2026-04-29 06:38:21 -06:00

95 lines
3.2 KiB
TOML

[project]
name = "mcaxl"
version = "2026.04.27.1"
description = "Read-only MCP server for Cisco Unified Communications Manager (CUCM) — AXL SOAP API + RisPort70 registration state — purpose-built for LLM-driven dial-plan and configuration auditing."
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.11"
keywords = [
"mcp", "cisco", "cucm", "axl", "risport",
"voip", "sip", "audit", "telephony",
]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: System Administrators",
"Intended Audience :: Telecommunications Industry",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Communications :: Telephony",
"Topic :: System :: Networking :: Monitoring",
]
dependencies = [
"fastmcp>=3.2",
"zeep>=4.3",
"platformdirs>=4.9",
"numpy>=1.26",
"python-dotenv>=1.0",
# SQL tokenizer for the executeSQLQuery validator. Using a real lexer
# (instead of regex keyword scanning) gives us proper string/comment
# boundary handling and explicit multi-statement detection. See
# src/mcaxl/sql_validator.py for the read-only enforcement layer.
"sqlparse>=0.5",
]
[project.optional-dependencies]
test = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
]
[project.scripts]
mcaxl = "mcaxl.server:main"
[project.urls]
Homepage = "https://git.supported.systems/mcp/mcaxl"
Source = "https://git.supported.systems/mcp/mcaxl"
Issues = "https://git.supported.systems/mcp/mcaxl/issues"
Changelog = "https://git.supported.systems/mcp/mcaxl/src/branch/main/CHANGELOG.md"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/mcaxl"]
[tool.hatch.build.targets.sdist]
# Keep the published source distribution focused on what's needed to
# build / install / run. Excluded files exist for local development only.
#
# IMPORTANT: this list is the last line of defense for PII leakage.
# `tests/` contains real cluster fixtures; `.mcp.json` contains a
# local filesystem path; `audits/` contains cluster-specific findings.
# Pre-publish workflow: extract the sdist to /tmp and grep for any
# site-specific tokens (your org name, internal IP ranges, hostnames)
# across the full unpacked tree — MUST return empty before publish.
exclude = [
"CLAUDE.md", # operator-private project context for Claude Code
".env", # never ship credentials
".env.local",
".mcp.json", # contains local filesystem path; dev-only artifact
"axlsqltoolkit.zip", # Cisco-licensed; do not redistribute
"audits/", # cluster-specific audit reports
"docs/", # Astro docs site — node_modules, build artifacts, dev container scaffolding
"tests/", # tests live in source repo, not the sdist
".pytest_cache/",
".ruff_cache/",
"dist/",
"build/",
]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"