sqlguard-ninja 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/CHANGELOG.md +11 -0
  2. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/PKG-INFO +20 -1
  3. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/README.md +15 -0
  4. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/IMPLEMENTATION_PROGRESS.md +2 -1
  5. sqlguard_ninja-0.4.0/docs/development/NEXT_ACTIONS.md +6 -0
  6. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/TEST_EVIDENCE.md +3 -0
  7. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/progress.json +8 -5
  8. sqlguard_ninja-0.4.0/docs/guides/MCP_INTEGRATION.md +77 -0
  9. sqlguard_ninja-0.4.0/docs/releases/v0.4.0.md +20 -0
  10. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/security/THREAT_MODEL.md +1 -0
  11. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/pyproject.toml +4 -2
  12. sqlguard_ninja-0.4.0/src/sqlguard_ninja/_version.py +1 -0
  13. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/cli/main.py +20 -0
  14. sqlguard_ninja-0.4.0/src/sqlguard_ninja/mcp_server.py +287 -0
  15. sqlguard_ninja-0.4.0/tests/integration/test_mcp_server.py +142 -0
  16. sqlguard_ninja-0.3.0/docs/development/NEXT_ACTIONS.md +0 -5
  17. sqlguard_ninja-0.3.0/src/sqlguard_ninja/_version.py +0 -1
  18. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/.gitignore +0 -0
  19. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/LICENSE +0 -0
  20. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/THIRD_PARTY_NOTICES.md +0 -0
  21. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/baselines/native.json +0 -0
  22. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/external/README.md +0 -0
  23. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/LABEL_CHANGES.yaml +0 -0
  24. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/01_destructive.yaml +0 -0
  25. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/02_multi_statement_and_formatting.yaml +0 -0
  26. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/03_table_and_column_access.yaml +0 -0
  27. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/04_privacy.yaml +0 -0
  28. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/05_tenant_isolation.yaml +0 -0
  29. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/06_cte_nested_unicode.yaml +0 -0
  30. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/07_legitimate_lookalike_cost_dialect.yaml +0 -0
  31. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/08_lineage.yaml +0 -0
  32. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/generated/grant_matrix.yaml +0 -0
  33. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/generated/label_matrix.yaml +0 -0
  34. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/generated/tenant_matrix.yaml +0 -0
  35. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/generated/transforms.yaml +0 -0
  36. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/manifest.yaml +0 -0
  37. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/policies/retail_saas.yaml +0 -0
  38. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/agent-sql-guard.json +0 -0
  39. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/agent-sql-guard.md +0 -0
  40. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/krish2105-sqlguard.json +0 -0
  41. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/krish2105-sqlguard.md +0 -0
  42. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/native.json +0 -0
  43. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/native.md +0 -0
  44. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/sql-guardrail.json +0 -0
  45. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/sql-guardrail.md +0 -0
  46. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/agent-sql-guard.csv +0 -0
  47. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/agent-sql-guard.json +0 -0
  48. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/agent-sql-guard.md +0 -0
  49. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/krish2105-sqlguard.csv +0 -0
  50. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/krish2105-sqlguard.json +0 -0
  51. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/krish2105-sqlguard.md +0 -0
  52. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/native.csv +0 -0
  53. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/native.json +0 -0
  54. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/native.md +0 -0
  55. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/sql-guardrail.csv +0 -0
  56. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/sql-guardrail.json +0 -0
  57. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/sql-guardrail.md +0 -0
  58. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/architecture/ARCHITECTURE_DECISIONS.md +0 -0
  59. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/benchmarks/RESULTS_v1.1.md +0 -0
  60. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/benchmarks/RESULTS_v1.md +0 -0
  61. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/ARCHITECTURE_MAP.md +0 -0
  62. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/DECISION_LOG.md +0 -0
  63. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/UPGRADE_MASTER_PLAN.md +0 -0
  64. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/AGENT_INTEGRATION.md +0 -0
  65. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/BENCHMARK_BIAS_REVIEW.md +0 -0
  66. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/BENCHMARK_METHODOLOGY.md +0 -0
  67. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/CI_INTEGRATION.md +0 -0
  68. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/POLICY_AS_CODE.md +0 -0
  69. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/POLICY_REFERENCE.md +0 -0
  70. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/QUERY_COST.md +0 -0
  71. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/release/FINAL_TECHNICAL_AUDIT.md +0 -0
  72. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/releases/v0.1.0.md +0 -0
  73. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/releases/v0.1.1.md +0 -0
  74. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/releases/v0.2.0.md +0 -0
  75. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/releases/v0.3.0.md +0 -0
  76. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/research/COMPETITIVE_ANALYSIS.md +0 -0
  77. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/security/STATIC_ANALYSIS_LIMITS.md +0 -0
  78. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/_shared.py +0 -0
  79. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/fastapi/app.py +0 -0
  80. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/github-actions/sql-security.yml +0 -0
  81. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/langchain/guarded_tool_demo.py +0 -0
  82. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/langgraph/real_llm_agent.py +0 -0
  83. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/langgraph/sql_agent_demo.py +0 -0
  84. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/postgresql/plan_inspection_demo.py +0 -0
  85. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/sdk/quickstart.py +0 -0
  86. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/sqlite/text_to_sql_demo.py +0 -0
  87. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/__init__.py +0 -0
  88. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/__init__.py +0 -0
  89. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/adapters.py +0 -0
  90. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/builtin.py +0 -0
  91. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/compare.py +0 -0
  92. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/dataset.py +0 -0
  93. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/fixtures.py +0 -0
  94. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/metrics.py +0 -0
  95. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/runner.py +0 -0
  96. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/translate.py +0 -0
  97. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/workers/_protocol.py +0 -0
  98. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/workers/agent_sql_guard_worker.py +0 -0
  99. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/workers/krish_sqlguard_worker.py +0 -0
  100. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/workers/sql_guardrail_worker.py +0 -0
  101. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/cli/__init__.py +0 -0
  102. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/cli/config.py +0 -0
  103. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/cli/templates.py +0 -0
  104. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/__init__.py +0 -0
  105. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/analysis.py +0 -0
  106. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/findings.py +0 -0
  107. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/guard.py +0 -0
  108. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/lineage.py +0 -0
  109. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/__init__.py +0 -0
  110. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/access.py +0 -0
  111. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/base.py +0 -0
  112. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/catalog.py +0 -0
  113. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/functions.py +0 -0
  114. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/statements.py +0 -0
  115. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/structure.py +0 -0
  116. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/tenant.py +0 -0
  117. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/__init__.py +0 -0
  118. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/audit.py +0 -0
  119. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/checkpoint.py +0 -0
  120. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/executors.py +0 -0
  121. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/fastapi.py +0 -0
  122. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/langchain.py +0 -0
  123. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/langgraph.py +0 -0
  124. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/__init__.py +0 -0
  125. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/inheritance.py +0 -0
  126. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/model.py +0 -0
  127. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/templates/analytics_readonly.yaml +0 -0
  128. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/templates/minimal.yaml +0 -0
  129. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/templates/multi_tenant_saas.yaml +0 -0
  130. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/tools.py +0 -0
  131. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/privacy/__init__.py +0 -0
  132. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/privacy/rules.py +0 -0
  133. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/privacy/suggest.py +0 -0
  134. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/py.typed +0 -0
  135. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/query_cost/__init__.py +0 -0
  136. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/query_cost/postgres.py +0 -0
  137. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/query_cost/static.py +0 -0
  138. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/reporting/__init__.py +0 -0
  139. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/reporting/markdown.py +0 -0
  140. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/tenancy.py +0 -0
  141. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/__init__.py +0 -0
  142. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/conftest.py +0 -0
  143. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/__init__.py +0 -0
  144. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/test_corpus_sqlite_validation.py +0 -0
  145. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/test_external_adapters.py +0 -0
  146. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/test_frameworks.py +0 -0
  147. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/test_postgres.py +0 -0
  148. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/pg_support.py +0 -0
  149. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/property/__init__.py +0 -0
  150. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/property/test_guard_properties.py +0 -0
  151. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/security/__init__.py +0 -0
  152. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/security/test_agent_boundaries.py +0 -0
  153. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/security/test_corpus_native.py +0 -0
  154. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/security/test_tenant_rewrite.py +0 -0
  155. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/__init__.py +0 -0
  156. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/helpers.py +0 -0
  157. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_benchmark_engine.py +0 -0
  158. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_cli.py +0 -0
  159. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_dataset.py +0 -0
  160. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_docs.py +0 -0
  161. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_function_policy_mechanics.py +0 -0
  162. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_lineage.py +0 -0
  163. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_policy.py +0 -0
  164. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_policy_inheritance.py +0 -0
  165. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_policy_tools.py +0 -0
  166. {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_rules.py +0 -0
@@ -3,6 +3,17 @@
3
3
  All notable changes are documented here. The project follows
4
4
  [Semantic Versioning](https://semver.org/); before 1.0, minor versions may change the API.
5
5
 
6
+ ## [0.4.0] — 2026-10-24
7
+
8
+ ### Added
9
+ * **MCP server** (`sqlninja mcp`, `sqlguard_ninja.mcp_server`, extra `[mcp]` on the official
10
+ MCP Python SDK 2.x). Tools: `validate_sql`, `explain_policy_decision`,
11
+ `inspect_query_cost`, `get_policy_capabilities`. The role and tenant are fixed server-side,
12
+ there is no execution and no database connection, internal detail is withheld, ungranted
13
+ tables and columns are not disclosed, input is size-capped, and calls are rate-limited.
14
+ * An end-to-end test drives the real server over stdio with the SDK's `ClientSession`.
15
+ * Docs: `docs/guides/MCP_INTEGRATION.md`; threat model T17.
16
+
6
17
  ## [0.3.0] — 2026-10-24
7
18
 
8
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlguard-ninja
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Security testing, policy enforcement, agent integration, privacy analysis and CI benchmarking for LLM-generated SQL.
5
5
  Project-URL: Homepage, https://github.com/Pratik653/SQLGuard_Ninjapy
6
6
  Project-URL: Documentation, https://github.com/Pratik653/SQLGuard_Ninjapy/tree/main/docs
@@ -29,6 +29,7 @@ Provides-Extra: all
29
29
  Requires-Dist: fastapi>=0.110; extra == 'all'
30
30
  Requires-Dist: langchain-core>=0.3; extra == 'all'
31
31
  Requires-Dist: langgraph>=0.2; extra == 'all'
32
+ Requires-Dist: mcp<3,>=2.3; extra == 'all'
32
33
  Requires-Dist: psycopg[binary]>=3.1; extra == 'all'
33
34
  Provides-Extra: dev
34
35
  Requires-Dist: build>=1.2; extra == 'dev'
@@ -37,6 +38,7 @@ Requires-Dist: httpx>=0.27; extra == 'dev'
37
38
  Requires-Dist: hypothesis>=6.100; extra == 'dev'
38
39
  Requires-Dist: langchain-core>=0.3; extra == 'dev'
39
40
  Requires-Dist: langgraph>=0.2; extra == 'dev'
41
+ Requires-Dist: mcp<3,>=2.3; extra == 'dev'
40
42
  Requires-Dist: mypy>=1.10; extra == 'dev'
41
43
  Requires-Dist: psycopg[binary]>=3.1; extra == 'dev'
42
44
  Requires-Dist: pytest-cov>=5.0; extra == 'dev'
@@ -50,6 +52,8 @@ Requires-Dist: langchain-core>=0.3; extra == 'langchain'
50
52
  Provides-Extra: langgraph
51
53
  Requires-Dist: langchain-core>=0.3; extra == 'langgraph'
52
54
  Requires-Dist: langgraph>=0.2; extra == 'langgraph'
55
+ Provides-Extra: mcp
56
+ Requires-Dist: mcp<3,>=2.3; extra == 'mcp'
53
57
  Provides-Extra: postgres
54
58
  Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
55
59
  Description-Content-Type: text/markdown
@@ -263,6 +267,20 @@ Optional: `SQLCheckpoint(..., rewrite_tenant_filters=True)` adds missing tenant
263
267
  through the AST and executes only the re-validated rewrite. See
264
268
  [POLICY_AS_CODE.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md).
265
269
 
270
+ ## MCP server
271
+
272
+ Let MCP-compatible agents check SQL against your policy before running it anywhere:
273
+
274
+ ```bash
275
+ python -m pip install "sqlguard-ninja[mcp]"
276
+ sqlninja mcp --policy builtin:retail_saas --role support_analyst --tenant 1
277
+ ```
278
+
279
+ The four tools are `validate_sql`, `explain_policy_decision`, `inspect_query_cost` and
280
+ `get_policy_capabilities`. The role and tenant are fixed server-side, the server never connects
281
+ to a database, and it exposes no execution tool. Tested with the official MCP Python SDK
282
+ client over stdio. See [MCP_INTEGRATION.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/MCP_INTEGRATION.md).
283
+
266
284
  ## Query cost analysis
267
285
 
268
286
  Every validation includes an explainable static score that uses the catalog's
@@ -358,6 +376,7 @@ writes, more tested dialects, and SARIF output. Details in [ROADMAP.md](https://
358
376
 
359
377
  * [Policy reference](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_REFERENCE.md)
360
378
  * [Policy as code](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md)
379
+ * [MCP integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/MCP_INTEGRATION.md)
361
380
  * [Agent integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/AGENT_INTEGRATION.md)
362
381
  * [Query cost](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/QUERY_COST.md)
363
382
  * [CI integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/CI_INTEGRATION.md)
@@ -207,6 +207,20 @@ Optional: `SQLCheckpoint(..., rewrite_tenant_filters=True)` adds missing tenant
207
207
  through the AST and executes only the re-validated rewrite. See
208
208
  [POLICY_AS_CODE.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md).
209
209
 
210
+ ## MCP server
211
+
212
+ Let MCP-compatible agents check SQL against your policy before running it anywhere:
213
+
214
+ ```bash
215
+ python -m pip install "sqlguard-ninja[mcp]"
216
+ sqlninja mcp --policy builtin:retail_saas --role support_analyst --tenant 1
217
+ ```
218
+
219
+ The four tools are `validate_sql`, `explain_policy_decision`, `inspect_query_cost` and
220
+ `get_policy_capabilities`. The role and tenant are fixed server-side, the server never connects
221
+ to a database, and it exposes no execution tool. Tested with the official MCP Python SDK
222
+ client over stdio. See [MCP_INTEGRATION.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/MCP_INTEGRATION.md).
223
+
210
224
  ## Query cost analysis
211
225
 
212
226
  Every validation includes an explainable static score that uses the catalog's
@@ -302,6 +316,7 @@ writes, more tested dialects, and SARIF output. Details in [ROADMAP.md](https://
302
316
 
303
317
  * [Policy reference](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_REFERENCE.md)
304
318
  * [Policy as code](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md)
319
+ * [MCP integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/MCP_INTEGRATION.md)
305
320
  * [Agent integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/AGENT_INTEGRATION.md)
306
321
  * [Query cost](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/QUERY_COST.md)
307
322
  * [CI integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/CI_INTEGRATION.md)
@@ -8,4 +8,5 @@
8
8
  | 2a Role inheritance (sticky denies), revision/fingerprint, decision ids, audit fields | VERIFIED (local) | 2f1919c | test_policy_inheritance (11) | |
9
9
  | 2b Policy diff/simulate/explain/templates CLI | VERIFIED (local) | e64859a | test_policy_tools (21) | |
10
10
  | 2c Opt-in AST tenant-filter rewrite + checkpoint | VERIFIED (local) | b0b5ade | test_tenant_rewrite (32, executes on SQLite, tenants 1-3) | |
11
- | 2d Docs, ADR-011/012, v0.3.0 bump | VERIFIED (local) | see PR | full gate 1,804 passed, 94% | |
11
+ | 2d Docs, ADR-011/012, v0.3.0 bump | RELEASED v0.3.0 | 198a308 | full gate 1,804 passed, 94% | |
12
+ | 3 MCP server (stdio, validation-only) | VERIFIED (local) | see PR | 9 tests incl. real SDK client | released as v0.4.0 (version targets adapted) |
@@ -0,0 +1,6 @@
1
+ # Next actions (development program)
2
+
3
+ 1. Merge the v0.4.0 (MCP) PR, tag, release, verify PyPI and a fresh install including the `[mcp]` extra and a stdio session.
4
+ 2. Phase 4 (v0.5.0): make MySQL a tested dialect against a MySQL 8 service container in CI (#11); SQL Server and Oracle parser-level experimental only; publish a compatibility matrix.
5
+ 3. Phase 5: OpenTelemetry/Prometheus extras, SARIF output (#14), Grafana dashboard JSON.
6
+ 4. Phases 6-8: playground UI on the FastAPI backend, Docker/Kubernetes reference, public demo (hosting needs maintainer authorization).
@@ -10,3 +10,6 @@
10
10
  | 2026-10-24 | v0.2.0 publish | release.yml run 40108113801 | build + publish success; wheel and sdist with PEP 740 provenance; 0.1.0 and 0.1.1 unchanged |
11
11
  | 2026-10-24 | v0.2.0 fresh install | `pip install --no-cache-dir --index-url https://pypi.org/simple sqlguard-ninja==0.2.0` (Python 3.13, outside repo) | import from site-packages; lineage allow/deny and tenant push-down via CLI; bundled benchmark 1,044 evaluations with CSV, tiers, 14 file hashes |
12
12
  | 2026-10-24 | v0.3.0 candidate | full gate | 1,804 passed, 94%; build + twine PASSED; 3 templates in wheel |
13
+ | 2026-10-24 | v0.3.0 release | PR #19 12/12; main CI on 198a308 success; release.yml run 40115952065 success | PyPI files with provenance |
14
+ | 2026-10-24 | v0.3.0 fresh install | pip from PyPI, Python 3.13, outside repo (3rd attempt; CDN lag) | templates, policy check/explain (inheritance), tenant rewrite, decision metadata OK |
15
+ | 2026-10-24 | v0.4.0 candidate (MCP) | tests/integration/test_mcp_server.py | 9 passed, including a real stdio ClientSession |
@@ -38,21 +38,24 @@
38
38
  {
39
39
  "id": 3,
40
40
  "name": "Policy-as-code and multitenancy",
41
- "status": "VERIFIED",
41
+ "status": "RELEASED",
42
42
  "release": "v0.3.0",
43
43
  "issues": [],
44
44
  "evidence": [
45
- "inheritance, diff/simulate/explain/templates, tenant rewrite, audit metadata; 1,804 tests pass locally"
45
+ "inheritance, diff/simulate/explain/templates, tenant rewrite, audit metadata; 1,804 tests pass locally",
46
+ "v0.3.0 on PyPI; fresh install verified"
46
47
  ],
47
48
  "blockers": []
48
49
  },
49
50
  {
50
51
  "id": 4,
51
52
  "name": "MCP server",
52
- "status": "NOT_STARTED",
53
- "release": "v0.3.0",
53
+ "status": "VERIFIED",
54
+ "release": "v0.4.0",
54
55
  "issues": [],
55
- "evidence": [],
56
+ "evidence": [
57
+ "stdio server; 9 tests incl. official SDK ClientSession"
58
+ ],
56
59
  "blockers": []
57
60
  },
58
61
  {
@@ -0,0 +1,77 @@
1
+ # MCP integration
2
+
3
+ SQLGuard Ninja ships a [Model Context Protocol](https://modelcontextprotocol.io/) server that
4
+ lets MCP-compatible agents check SQL against your policy **before** they run it anywhere.
5
+
6
+ ```bash
7
+ python -m pip install "sqlguard-ninja[mcp]"
8
+ sqlninja mcp --policy policy.yaml --role support --tenant 42 # stdio transport
9
+ ```
10
+
11
+ Built on the official MCP Python SDK (`mcp` 2.x, `MCPServer`). The bundled reference policy
12
+ works out of the box: `--policy builtin:retail_saas --role support_analyst --tenant 1`.
13
+
14
+ ## Tools
15
+
16
+ | Tool | Arguments | Returns |
17
+ |---|---|---|
18
+ | `validate_sql` | `sql`, optional `dialect` (`postgres` or `sqlite`) | `decision` (allow/review/deny), findings (rule id, action, message, remediation), `decision_id`, policy revision |
19
+ | `explain_policy_decision` | `sql`, optional `dialect` | the decision plus per-finding lineage (base column, JSON path, labels) and how-to-fix hints |
20
+ | `inspect_query_cost` | `sql`, optional `dialect` | static cost score, thresholds and factors (no execution plan, no database) |
21
+ | `get_policy_capabilities` | — | the tables and columns this identity may use, their access mode (`allow`, `aggregate_only`, `review`), required tenant filter, limits, supported dialects |
22
+
23
+ No tool accepts a role, a tenant, credentials or a connection string. The server has **no
24
+ execution tool** and never connects to a database.
25
+
26
+ ## Security model
27
+
28
+ * **Identity is server-side.** `--role` and `--tenant` are fixed when the server starts, by
29
+ whoever writes the client configuration. A model cannot choose its own permissions. Extra
30
+ arguments such as `role` are rejected or ignored, and the end-to-end test checks this.
31
+ * **Least disclosure.** `get_policy_capabilities` lists only tables and columns the role may
32
+ use. Denied tables and columns are not revealed. Internal finding detail is never returned,
33
+ and with `disclosure: generic` lineage is omitted as well.
34
+ * **Limits.** A hard cap of 100,000 characters applies before parsing, the policy's own length
35
+ and AST-size limits still apply, and a per-process rate limit (120 calls per minute by
36
+ default) is enforced. Errors are structured (`{"error": ..., "message": ...}`) and carry no
37
+ stack traces.
38
+ * **Fail closed.** A tenant-scoped role started without `--tenant` gets every tenant-table
39
+ query denied.
40
+ * The server is a validation service. Running the SQL remains your application's job, with a
41
+ least-privilege, read-only database role and row-level security.
42
+
43
+ ## Client configuration (stdio)
44
+
45
+ Most MCP clients accept a JSON server definition of this shape. Adapt the command path to
46
+ your environment:
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "sqlguard-ninja": {
52
+ "command": "sqlninja",
53
+ "args": ["mcp", "--policy", "/absolute/path/policy.yaml", "--role", "support", "--tenant", "42"]
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ One server process serves one identity. To give different users different permissions, start
60
+ one server per identity from your trusted configuration. Never let the model write the client
61
+ configuration.
62
+
63
+ ## Remote transports
64
+
65
+ The SDK also supports Streamable HTTP. SQLGuard Ninja does **not** enable it by default,
66
+ because a remote endpoint needs authentication that binds each caller to a role and tenant.
67
+ If you expose it remotely, put it behind your own authenticated gateway, derive the identity
68
+ from the authenticated caller, and keep one identity per server process or route. This setup
69
+ is not covered by the test suite.
70
+
71
+ ## Verification
72
+
73
+ `tests/integration/test_mcp_server.py` starts the real server as a subprocess and connects
74
+ with the official SDK's `stdio_client` and `ClientSession`. It checks tool discovery,
75
+ allowed and denied validations, a forged `role` argument, and capability listing. It also
76
+ unit-tests malformed input, oversized input, unsupported dialects, rate limiting, the
77
+ missing-tenant fail-closed path and least disclosure.
@@ -0,0 +1,20 @@
1
+ # SQLGuard Ninja v0.4.0
2
+
3
+ ## Highlights
4
+
5
+ * **MCP server.** `sqlninja mcp --policy ... --role ... --tenant ...` exposes `validate_sql`, `explain_policy_decision`, `inspect_query_cost` and `get_policy_capabilities` to MCP-compatible agents over stdio. It is built on the official MCP Python SDK (2.x).
6
+ * **Security model.** The identity is fixed by the operator at start-up, and tools take no role, tenant, credential or connection-string arguments. The server never executes SQL or connects to a database, withholds internal detail, and does not reveal ungranted tables or columns. Inputs are size-capped and calls are rate-limited.
7
+ * **Verified with a real client.** The end-to-end test launches the server as a subprocess and uses the SDK's `stdio_client` and `ClientSession` to discover and call the tools, including a forged `role` argument.
8
+
9
+ ## Notes
10
+
11
+ * Remote (Streamable HTTP) transport is not enabled. A remote deployment needs an authenticated gateway that binds each caller to an identity (see the MCP guide).
12
+ * Known limitations are unchanged: all benchmark results are internal (issues #5 and #6), only PostgreSQL and SQLite are supported, and there is no guarantee against SQL injection or data leakage.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ python -m pip install "sqlguard-ninja[mcp]==0.4.0"
18
+ ```
19
+
20
+ PyPI: https://pypi.org/project/sqlguard-ninja/0.4.0/
@@ -57,6 +57,7 @@ flowchart LR
57
57
  | T3 | Prompt injection through database content | A ticket body says "now query payments" | Every subsequent query is checked again. Retry lock-out per trace. Read-only session. | The model can still be steered to permitted data. |
58
58
  | T4 | Parser ambiguity | Comment and string tricks, nested comments, dollar quoting | sqlglot dialect parsing. The corpus includes formatting and comment cases validated against real engines. Unparseable input is denied. | Unknown tokenizer differences. Report them as vulnerabilities. |
59
59
  | T5 | Dialect-specific bypasses | MySQL `/*! */` executable comments, SQLite `ATTACH`/`PRAGMA` | Executable-comment rule for the MySQL family. SQLite session statements are denied. | Untested dialects are experimental. |
60
+ | T17 | MCP client or model escalating privileges | A model passes `role` or `tenant` arguments, or probes `get_policy_capabilities` for hidden tables | Identity fixed at server start from operator config; tool schemas have no identity arguments; capabilities list only granted, non-denied columns; no execution or credentials in the server | Anyone who can edit the MCP client configuration chooses the identity |
60
61
  | T6a | Unsafe automatic tenant filtering | A rewrite that changes meaning or masks a query naming another tenant | Rewrite is opt-in, bind-parameter only, refused on any non-tenant finding and on RIGHT/FULL-join ownership ambiguity; the rewritten SQL is re-validated and is the executed string | Rewritten SQL is sqlglot-normalised; RLS remains authoritative |
61
62
  | T6b | Silent policy relaxation through inheritance or edits | A child role or edit quietly widens access | Sticky explicit denies (load error on conflict); `policy diff --fail-on-relaxation`; fingerprint and revision in every decision | Reviewers must still read the diff |
62
63
  | T6 | Tenant data leakage | missing filter, `OR 1=1`, other tenant id, unpinned join or subquery | Every scope reading a tenant table must pin it with an equality conjunct to the context value or an approved bind parameter. `OR`/`NOT` don't count, and casts of the column don't count. | Apply RLS in the database as the authoritative control. |
@@ -36,8 +36,10 @@ dependencies = [
36
36
  langchain = ["langchain-core>=0.3"]
37
37
  langgraph = ["langgraph>=0.2", "langchain-core>=0.3"]
38
38
  fastapi = ["fastapi>=0.110"]
39
+ # Official MCP Python SDK; 2.x renamed FastMCP to MCPServer.
40
+ mcp = ["mcp>=2.3,<3"]
39
41
  postgres = ["psycopg[binary]>=3.1"]
40
- all = ["sqlguard-ninja[langchain,langgraph,fastapi,postgres]"]
42
+ all = ["sqlguard-ninja[langchain,langgraph,fastapi,postgres,mcp]"]
41
43
  dev = [
42
44
  "sqlguard-ninja[all]",
43
45
  "pytest>=8.0",
@@ -118,5 +120,5 @@ exclude = ["src/sqlguard_ninja/benchmarking/workers/"]
118
120
  plugins = ["pydantic.mypy"]
119
121
 
120
122
  [[tool.mypy.overrides]]
121
- module = ["langgraph.*", "langchain_core.*", "psycopg.*", "pgserver.*"]
123
+ module = ["langgraph.*", "langchain_core.*", "psycopg.*", "pgserver.*", "mcp.*"]
122
124
  ignore_missing_imports = true
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -273,6 +273,19 @@ def cmd_policy_explain(args: argparse.Namespace) -> int:
273
273
  return EXIT_OK
274
274
 
275
275
 
276
+ def cmd_mcp(args: argparse.Namespace) -> int: # pragma: no cover - blocks on stdio
277
+ try:
278
+ from sqlguard_ninja.mcp_server import ServerIdentity, serve # noqa: PLC0415
279
+ except ImportError as exc:
280
+ raise CLIError('the MCP server needs: pip install "sqlguard-ninja[mcp]"') from exc
281
+ policy = _load_policy(args.policy)
282
+ try:
283
+ serve(policy, ServerIdentity(role=args.role, tenant=args.tenant), args.dialect)
284
+ except ValueError as exc:
285
+ raise CLIError(str(exc)) from exc
286
+ return EXIT_OK
287
+
288
+
276
289
  def cmd_init(args: argparse.Namespace) -> int:
277
290
  root = Path(args.directory)
278
291
  existing = [name for name in FILES if (root / name).exists()]
@@ -558,6 +571,13 @@ def build_parser() -> argparse.ArgumentParser:
558
571
  bb.add_argument("--i-understand-this-is-ci", action="store_true", help=argparse.SUPPRESS)
559
572
  bb.set_defaults(func=cmd_benchmark_baseline)
560
573
 
574
+ mcp = sub.add_parser("mcp", help="run the MCP server (stdio) for a fixed role and tenant")
575
+ mcp.add_argument("--policy", required=True, help="policy file or builtin:<name>")
576
+ mcp.add_argument("--role", required=True, help="trusted role for every request")
577
+ mcp.add_argument("--tenant", help="trusted tenant for every request")
578
+ mcp.add_argument("--dialect")
579
+ mcp.set_defaults(func=cmd_mcp)
580
+
561
581
  rep = sub.add_parser("report", help="reports").add_subparsers(dest="report_cmd", required=True)
562
582
  rg = rep.add_parser("generate", help="render a Markdown report from a result JSON")
563
583
  rg.add_argument("--result", required=True)
@@ -0,0 +1,287 @@
1
+ """Model Context Protocol (MCP) server exposing SQLGuard Ninja as a validation service.
2
+
3
+ Security model:
4
+
5
+ * **Identity is server-side.** The role and tenant are fixed when the server starts, from the
6
+ operator's configuration (for example an MCP client config). No tool accepts a role, a
7
+ tenant or an approval, so a model cannot choose its own permissions.
8
+ * **No execution, no credentials.** The server never connects to a database. It has no
9
+ connection-string arguments and no filesystem tools. It only validates and explains SQL.
10
+ * **Agent-safe output.** Tools return the agent-facing messages and remediations. Internal
11
+ detail is omitted, and lineage only names columns the role is allowed to know about.
12
+ * **Resource limits.** A hard input-size cap applies before parsing, plus a per-process rate
13
+ limit.
14
+
15
+ Run: ``sqlninja mcp --policy policy.yaml --role support --tenant 42`` (stdio transport).
16
+ Requires ``pip install "sqlguard-ninja[mcp]"``.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import threading
22
+ import time
23
+ from collections import deque
24
+ from dataclasses import dataclass
25
+ from typing import Any
26
+
27
+ from sqlguard_ninja._version import __version__
28
+ from sqlguard_ninja.core.analysis import SUPPORTED_DIALECTS, analyse
29
+ from sqlguard_ninja.core.findings import ValidationResult
30
+ from sqlguard_ninja.core.guard import Guard
31
+ from sqlguard_ninja.policy.model import Access, Disclosure, Policy
32
+ from sqlguard_ninja.privacy.rules import resolve_column_access
33
+ from sqlguard_ninja.query_cost.static import analyse_static_cost
34
+
35
+ HARD_MAX_SQL_CHARS = 100_000
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ServerIdentity:
40
+ """Trusted request identity, configured by the operator, never by the model."""
41
+
42
+ role: str
43
+ tenant: str | None = None
44
+ principal: str = "mcp-client"
45
+
46
+
47
+ class RateLimiter:
48
+ """Sliding-window limiter: at most ``limit`` calls per ``window_s`` seconds."""
49
+
50
+ def __init__(self, limit: int = 120, window_s: float = 60.0) -> None:
51
+ self.limit = limit
52
+ self.window_s = window_s
53
+ self._calls: deque[float] = deque()
54
+ self._lock = threading.Lock()
55
+
56
+ def allow(self) -> bool:
57
+ now = time.monotonic()
58
+ with self._lock:
59
+ while self._calls and now - self._calls[0] > self.window_s:
60
+ self._calls.popleft()
61
+ if len(self._calls) >= self.limit:
62
+ return False
63
+ self._calls.append(now)
64
+ return True
65
+
66
+
67
+ class SQLGuardService:
68
+ """Transport-independent tool implementations (unit-testable without MCP)."""
69
+
70
+ def __init__(
71
+ self,
72
+ policy: Policy,
73
+ identity: ServerIdentity,
74
+ *,
75
+ dialect: str | None = None,
76
+ rate_limiter: RateLimiter | None = None,
77
+ ) -> None:
78
+ if policy.roles and policy.effective_role(identity.role) is None:
79
+ raise ValueError(f"role {identity.role!r} is not defined in the policy")
80
+ self.policy = policy
81
+ self.guard = Guard(policy)
82
+ self.identity = identity
83
+ self.dialect = (dialect or policy.dialect).lower()
84
+ self.rate_limiter = rate_limiter or RateLimiter()
85
+
86
+ # ------------------------------------------------------------------ helpers
87
+ def _context(self) -> dict[str, Any]:
88
+ if self.identity.tenant is None:
89
+ return {}
90
+ return {self.policy.tenant.context_key: self.identity.tenant}
91
+
92
+ def _precheck(self, sql: Any, dialect: str | None) -> tuple[str, str] | dict[str, Any]:
93
+ if not self.rate_limiter.allow():
94
+ return {"error": "rate_limited", "message": "Too many requests; try again shortly."}
95
+ if not isinstance(sql, str):
96
+ return {"error": "invalid_input", "message": "sql must be a string."}
97
+ if len(sql) > HARD_MAX_SQL_CHARS:
98
+ return {"error": "invalid_input", "message": "sql is too large."}
99
+ chosen = (dialect or self.dialect).lower()
100
+ if chosen not in SUPPORTED_DIALECTS:
101
+ return {
102
+ "error": "unsupported_dialect",
103
+ "message": f"Supported dialects: {', '.join(SUPPORTED_DIALECTS)}.",
104
+ }
105
+ return sql, chosen
106
+
107
+ def _validate(self, sql: str, dialect: str) -> ValidationResult:
108
+ return self.guard.validate(
109
+ sql, role=self.identity.role, dialect=dialect, context=self._context()
110
+ )
111
+
112
+ def _public_findings(
113
+ self, result: ValidationResult, with_lineage: bool
114
+ ) -> list[dict[str, Any]]:
115
+ out = []
116
+ generic = self.policy.disclosure == Disclosure.GENERIC
117
+ for f in result.findings:
118
+ item: dict[str, Any] = {
119
+ "rule_id": f.rule_id,
120
+ "action": f.action.value,
121
+ "category": f.category.value,
122
+ "message": f.message,
123
+ }
124
+ if f.remediation:
125
+ item["remediation"] = f.remediation
126
+ if with_lineage and not generic and f.metadata.get("lineage"):
127
+ lineage = f.metadata["lineage"]
128
+ item["lineage"] = {
129
+ "base_column": lineage.get("base_column"),
130
+ "json_path": lineage.get("json_path"),
131
+ "labels": lineage.get("labels"),
132
+ }
133
+ out.append(item)
134
+ return out
135
+
136
+ # -------------------------------------------------------------------- tools
137
+ def validate_sql(self, sql: Any, dialect: str | None = None) -> dict[str, Any]:
138
+ checked = self._precheck(sql, dialect)
139
+ if isinstance(checked, dict):
140
+ return checked
141
+ result = self._validate(*checked)
142
+ return {
143
+ "decision": result.decision.value,
144
+ "allowed": result.allowed,
145
+ "findings": self._public_findings(result, with_lineage=False),
146
+ "decision_id": result.decision_id,
147
+ "policy_revision": result.policy_revision,
148
+ "note": "Validation only: this server never executes SQL. Database permissions "
149
+ "remain authoritative.",
150
+ }
151
+
152
+ def explain_policy_decision(self, sql: Any, dialect: str | None = None) -> dict[str, Any]:
153
+ checked = self._precheck(sql, dialect)
154
+ if isinstance(checked, dict):
155
+ return checked
156
+ result = self._validate(*checked)
157
+ return {
158
+ "decision": result.decision.value,
159
+ "decision_id": result.decision_id,
160
+ "role": self.identity.role,
161
+ "tenant_scoped": bool(self.identity.tenant),
162
+ "policy": {"name": result.policy_name, "revision": result.policy_revision},
163
+ "findings": self._public_findings(result, with_lineage=True),
164
+ "how_to_fix": [f.remediation for f in result.findings if f.remediation],
165
+ }
166
+
167
+ def inspect_query_cost(self, sql: Any, dialect: str | None = None) -> dict[str, Any]:
168
+ checked = self._precheck(sql, dialect)
169
+ if isinstance(checked, dict):
170
+ return checked
171
+ text, chosen = checked
172
+ limits = self.policy.effective_limits(self.policy.role(self.identity.role)[1])
173
+ analysis = analyse(text, chosen, self.policy.catalog, max_nodes=limits.max_ast_nodes)
174
+ if not analysis.ok or analysis.qualified is None:
175
+ return {"error": "unanalysable", "message": "The query could not be analysed."}
176
+ report = analyse_static_cost(analysis, self.policy)
177
+ return {
178
+ "static_score": report.score,
179
+ "review_threshold": self.policy.cost.review_score,
180
+ "deny_threshold": self.policy.cost.deny_score,
181
+ "factors": [
182
+ {"code": f.code, "points": f.points, "explanation": f.explanation}
183
+ for f in report.factors
184
+ ],
185
+ "note": "Static heuristic only; no execution plan is requested from any database.",
186
+ }
187
+
188
+ def get_policy_capabilities(self) -> dict[str, Any]:
189
+ """What this identity may query: permitted tables/columns and how they may be used."""
190
+ if not self.rate_limiter.allow():
191
+ return {"error": "rate_limited", "message": "Too many requests; try again shortly."}
192
+ role = self.policy.effective_role(self.identity.role) if self.policy.roles else None
193
+ tables: dict[str, Any] = {}
194
+ for tname, tdef in self.policy.catalog.tables.items():
195
+ grant = role.tables.get(tname) if role is not None else None
196
+ if role is not None and grant is None:
197
+ continue # do not reveal tables the role cannot use
198
+ columns = {}
199
+ for cname in tdef.columns:
200
+ listed = grant is None or grant.columns == "*" or cname in grant.columns
201
+ if not listed or (grant is not None and cname in grant.deny_columns):
202
+ continue
203
+ access = resolve_column_access(self.policy, role, tname, cname).strongest
204
+ if access is Access.DENY:
205
+ continue
206
+ columns[cname] = access.value
207
+ needs_pin = bool(tdef.tenant_column) and (role is None or role.tenant_scoped)
208
+ tables[tname] = {
209
+ "columns": columns,
210
+ "tenant_filter_required": tdef.tenant_column if needs_pin else None,
211
+ }
212
+ limits = self.policy.effective_limits(role)
213
+ return {
214
+ "role": self.identity.role,
215
+ "policy": {"name": self.policy.name, "revision": self.policy.revision},
216
+ "dialects": list(SUPPORTED_DIALECTS),
217
+ "default_dialect": self.dialect,
218
+ "read_only": True,
219
+ "tenant_bind_parameter": self.policy.tenant.parameter_names[0]
220
+ if self.policy.tenant.parameter_names
221
+ else None,
222
+ "limits": {"max_limit": limits.max_limit, "max_joins": limits.max_joins},
223
+ "tables": tables,
224
+ "column_access_values": {
225
+ "allow": "may be selected and filtered",
226
+ "aggregate_only": "only inside COUNT/SUM/AVG-style aggregates",
227
+ "review": "requires human approval",
228
+ },
229
+ }
230
+
231
+
232
+ def build_server(service: SQLGuardService) -> Any:
233
+ """Create the MCP server with the four tools bound to ``service``."""
234
+ from mcp.server.mcpserver import MCPServer # noqa: PLC0415 - optional dependency
235
+
236
+ server = MCPServer(
237
+ name="sqlguard-ninja",
238
+ version=__version__,
239
+ instructions=(
240
+ "Validate SQL before running it. The server enforces a fixed role and tenant "
241
+ "chosen by the operator; you cannot change them. It never executes SQL."
242
+ ),
243
+ )
244
+
245
+ @server.tool(description="Validate one SQL statement against the configured policy.")
246
+ def validate_sql(sql: str, dialect: str | None = None) -> dict[str, Any]:
247
+ return service.validate_sql(sql, dialect)
248
+
249
+ @server.tool(description="Explain why a SQL statement is allowed, denied or needs review.")
250
+ def explain_policy_decision(sql: str, dialect: str | None = None) -> dict[str, Any]:
251
+ return service.explain_policy_decision(sql, dialect)
252
+
253
+ @server.tool(description="Estimate query cost risk with static heuristics (no execution).")
254
+ def inspect_query_cost(sql: str, dialect: str | None = None) -> dict[str, Any]:
255
+ return service.inspect_query_cost(sql, dialect)
256
+
257
+ @server.tool(description="List the tables, columns and rules available to this identity.")
258
+ def get_policy_capabilities() -> dict[str, Any]:
259
+ return service.get_policy_capabilities()
260
+
261
+ return server
262
+
263
+
264
+ def serve(
265
+ policy: Policy, identity: ServerIdentity, dialect: str | None = None
266
+ ) -> None: # pragma: no cover - exercised through the stdio integration test
267
+ build_server(SQLGuardService(policy, identity, dialect=dialect)).run("stdio")
268
+
269
+
270
+ def main(argv: list[str] | None = None) -> int: # pragma: no cover - thin CLI wrapper
271
+ import argparse # noqa: PLC0415
272
+
273
+ from sqlguard_ninja.benchmarking.builtin import resolve # noqa: PLC0415
274
+
275
+ p = argparse.ArgumentParser(prog="sqlguard-ninja-mcp")
276
+ p.add_argument("--policy", required=True)
277
+ p.add_argument("--role", required=True)
278
+ p.add_argument("--tenant")
279
+ p.add_argument("--dialect")
280
+ args = p.parse_args(argv)
281
+ policy = Policy.from_file(resolve(args.policy, "policy"))
282
+ serve(policy, ServerIdentity(role=args.role, tenant=args.tenant), args.dialect)
283
+ return 0
284
+
285
+
286
+ if __name__ == "__main__": # pragma: no cover
287
+ raise SystemExit(main())