mcp-capdiff 0.1.1__tar.gz → 0.1.3__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 (110) hide show
  1. mcp_capdiff-0.1.3/CHANGELOG.md +73 -0
  2. {mcp_capdiff-0.1.1/src/mcp_capdiff.egg-info → mcp_capdiff-0.1.3}/PKG-INFO +1 -1
  3. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/precision-matrix.md +4 -2
  4. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/releasing.md +1 -1
  5. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/versioning.md +1 -1
  6. mcp_capdiff-0.1.3/fixtures/benchmark_semantics/server.py +88 -0
  7. mcp_capdiff-0.1.3/fixtures/filesystem_helper_restricted/server.py +32 -0
  8. mcp_capdiff-0.1.3/fixtures/local_filesystem_operations/server.py +31 -0
  9. mcp_capdiff-0.1.3/fixtures/programmatic_registration/fixture_tools/__init__.py +1 -0
  10. mcp_capdiff-0.1.3/fixtures/programmatic_registration/fixture_tools/filesystem.py +23 -0
  11. mcp_capdiff-0.1.3/fixtures/programmatic_registration/fixture_tools/state.py +3 -0
  12. mcp_capdiff-0.1.3/fixtures/programmatic_registration/server.py +10 -0
  13. mcp_capdiff-0.1.3/fixtures/sqlite_read_only/server.py +18 -0
  14. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/__init__.py +1 -1
  15. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/rules/engine.py +7 -6
  16. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/scanners/source/python.py +382 -31
  17. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3/src/mcp_capdiff.egg-info}/PKG-INFO +1 -1
  18. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/SOURCES.txt +9 -0
  19. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/scan.json +1 -1
  20. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/scan.sarif.json +1 -1
  21. mcp_capdiff-0.1.3/tests/test_real_world_regressions.py +92 -0
  22. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_reporters.py +2 -2
  23. mcp_capdiff-0.1.1/CHANGELOG.md +0 -40
  24. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/LICENSE +0 -0
  25. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/MANIFEST.in +0 -0
  26. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/README.md +0 -0
  27. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/SECURITY.md +0 -0
  28. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/action.yml +0 -0
  29. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/policy.md +0 -0
  30. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/report-schema.md +0 -0
  31. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP001.md +0 -0
  32. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP002.md +0 -0
  33. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP003.md +0 -0
  34. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP004.md +0 -0
  35. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP005.md +0 -0
  36. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP007.md +0 -0
  37. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP010.md +0 -0
  38. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP016.md +0 -0
  39. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP017.md +0 -0
  40. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/README.md +0 -0
  41. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/v0.1-scope.md +0 -0
  42. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/.github/workflows/mcp-audit.yml +0 -0
  43. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/README.md +0 -0
  44. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/mcp-audit.yaml +0 -0
  45. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/pyproject.toml +0 -0
  46. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/approval_removed/after.py +0 -0
  47. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/approval_removed/before.py +0 -0
  48. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/filesystem_expansion/after.py +0 -0
  49. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/filesystem_expansion/before.py +0 -0
  50. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/network_allowlist_added/after.py +0 -0
  51. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/network_allowlist_added/before.py +0 -0
  52. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/unrestricted_fetch/after.py +0 -0
  53. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/unrestricted_fetch/before.py +0 -0
  54. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/destructive_with_approval/server.py +0 -0
  55. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/destructive_without_approval/server.py +0 -0
  56. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/fastmcp_async_annotations/server.py +0 -0
  57. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/filesystem_guard_after_read/server.py +0 -0
  58. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/filesystem_restricted/server.py +0 -0
  59. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/filesystem_unrestricted/server.py +0 -0
  60. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/postgresql_read_only/server.py +0 -0
  61. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/safe_server/server.py +0 -0
  62. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/sensitive_read_only/server.py +0 -0
  63. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/sensitive_to_network/server.py +0 -0
  64. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/shell_direct/server.py +0 -0
  65. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/shell_dynamic_no_shell/server.py +0 -0
  66. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/shell_safe_allowlist/server.py +0 -0
  67. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/sql_destructive/server.py +0 -0
  68. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/url_allowlisted/server.py +0 -0
  69. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/url_arbitrary/server.py +0 -0
  70. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/url_guard_after_request/server.py +0 -0
  71. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/url_hostname_unguarded/server.py +0 -0
  72. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/vulnerable_server/server.py +0 -0
  73. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/mcp-audit.yaml +0 -0
  74. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/pyproject.toml +0 -0
  75. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/setup.cfg +0 -0
  76. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/__main__.py +0 -0
  77. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/analysis/__init__.py +0 -0
  78. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/analysis/diff.py +0 -0
  79. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/analysis/scan.py +0 -0
  80. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/cli/__init__.py +0 -0
  81. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/cli/main.py +0 -0
  82. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/models/__init__.py +0 -0
  83. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/models/capability.py +0 -0
  84. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/models/finding.py +0 -0
  85. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/models/report.py +0 -0
  86. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/policy/__init__.py +0 -0
  87. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/policy/evaluator.py +0 -0
  88. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/policy/loader.py +0 -0
  89. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/__init__.py +0 -0
  90. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/json.py +0 -0
  91. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/manifest.py +0 -0
  92. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/sarif.py +0 -0
  93. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/terminal.py +0 -0
  94. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/rules/__init__.py +0 -0
  95. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/scanners/__init__.py +0 -0
  96. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/scanners/source/__init__.py +0 -0
  97. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/dependency_links.txt +0 -0
  98. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/entry_points.txt +0 -0
  99. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/requires.txt +0 -0
  100. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/top_level.txt +0 -0
  101. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/diff.txt +0 -0
  102. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/manifest.yaml +0 -0
  103. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/terminal.txt +0 -0
  104. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_demo_scenarios.py +0 -0
  105. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_diff.py +0 -0
  106. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_discovery.py +0 -0
  107. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_golden.py +0 -0
  108. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_policy.py +0 -0
  109. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_rule_accuracy.py +0 -0
  110. {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_scan.py +0 -0
@@ -0,0 +1,73 @@
1
+ # Changelog
2
+
3
+ All notable changes follow Keep a Changelog. Releases use semantic versioning for the CLI package; rules and output schemas are versioned independently.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.3] - 2026-10-01
8
+
9
+ ### Fixed
10
+
11
+ - Stop ordinary string, datetime, and other object `.replace()` calls from being classified as filesystem writes; retain detection for `os.replace(...)` and statically known `Path.replace(...)` calls.
12
+ - Detect model-controlled interactive process execution through `pexpect.spawn(...)` and `pexpect.spawnu(...)`.
13
+ - Require action verbs for semantic external-write inference so nouns such as `email` and `message` do not turn list/read tools into writes.
14
+ - Prefer read-only tool names and explicit negation over incidental destructive words in descriptions while retaining destructive SQL detection for execution tools.
15
+
16
+ ### Verified
17
+
18
+ - 71 automated tests, including positive controls for shell execution, filesystem replacement, destructive actions, and external writes.
19
+ - `metabase-mcp` and `mcp_agent_mail`: string and datetime replacement no longer produce MCP002 findings.
20
+ - `interactive-terminal-mcp`: `spawn_process` is classified as shell execution and produces MCP001 evidence at `pexpect.spawn(...)`.
21
+ - `MCP-PostgreSQL-Ops` and `winremote-mcp`: the read-only statistics tool and GUI `Type` tool no longer produce MCP004/MCP010 findings.
22
+ - The 14-repository benchmark covered 888 discovered tools; interprocedural network propagation remains deferred for a later release.
23
+
24
+ ## [0.1.2] - 2026-10-01
25
+
26
+ ### Fixed
27
+
28
+ - Discover tools registered with `mcp.tool()(function)`, including functions imported from scanned project modules; unresolved static registrations now fail closed.
29
+ - Require a concrete outbound network destination before MCP005 can classify a tool as an exfiltration sink.
30
+ - Propagate approved-root guarantees from path-validation helpers such as `_resolve(path)` to later filesystem operations.
31
+ - Classify `open(..., "w")`, directory creation, and local filesystem mutations as local writes instead of reads or external writes.
32
+
33
+ ### Verified
34
+
35
+ - `python-fastmcp-server`: 3 programmatically registered tools discovered instead of a zero-tool result.
36
+ - `sqlite-explorer-fastmcp-mcp-server`: 3 read-only tools, 0 side-effect or MCP005 findings.
37
+ - `obsidian-mcp`: helper-scoped filesystem tools recognized as allowlisted; only 3 findings remain.
38
+ - `smart_terminal_mcp`: critical execution findings preserved, write-mode evidence corrected, and no MCP005 paths remain.
39
+
40
+ ## [0.1.1] - 2026-10-01
41
+
42
+ ### Added
43
+
44
+ - Dedicated PyPI Trusted Publishing workflow with tag-pinned builds and OIDC authentication.
45
+
46
+ ### Changed
47
+
48
+ - Rename the PyPI distribution to `mcp-capdiff` while preserving the MCP Audit product name, `mcp_audit` import package, and `mcp-audit` command.
49
+ - Prepare version `0.1.1` so the public `v0.1.0` tag remains immutable.
50
+
51
+ ## [0.1.0] - 2026-10-01
52
+
53
+ ### Fixed
54
+
55
+ - Prevent `PostgreSQL` from being misclassified as an HTTP `POST` side effect.
56
+ - Attach destructive semantic evidence to MCP004 and MCP010 instead of unrelated database-read evidence.
57
+ - Fail closed on inaccessible source trees and zero-tool scans instead of reporting a false PASS.
58
+
59
+ ### Added
60
+
61
+ - Discovery benchmark for async `@mcp.tool()` functions with FastMCP `ToolAnnotations`.
62
+ - Explicit `--allow-empty` escape hatch for intentional zero-tool scans.
63
+ - Python/FastMCP source discovery and capability extraction.
64
+ - Nine security and regression rules in ruleset `0.1`.
65
+ - Cross-tool sensitive-data path detection with MCP-context isolation.
66
+ - Terminal, JSON, SARIF, manifest, and Git security-diff output.
67
+ - Versioned report and manifest schema `1.0`.
68
+ - Policy thresholds and reasoned, expiring suppressions.
69
+ - Composite GitHub Action with Code Scanning upload.
70
+
71
+ ### Verified
72
+
73
+ - External PostgreSQL MCP benchmark: 20 tools discovered, 6 legitimate findings, no known false positives from `PostgreSQL` token matching, and no false PASS.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-capdiff
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Security regression scanner for Model Context Protocol servers.
5
5
  Author: MCP Audit contributors
6
6
  License-Expression: MIT
@@ -4,8 +4,8 @@ The release gate requires positive, negative, guarded, and edge coverage for the
4
4
 
5
5
  | Rule | True positive | True negative | Guarded safe | Tricky edge |
6
6
  | --- | --- | --- | --- | --- |
7
- | MCP001 | `shell_direct` | `safe_server` | `shell_safe_allowlist` | `shell_dynamic_no_shell` detects a dynamic executable without `shell=True` |
8
- | MCP002 | `filesystem_unrestricted` | `sensitive_read_only` | `filesystem_restricted` | `filesystem_guard_after_read` rejects a late guard |
7
+ | MCP001 | `shell_direct` | `safe_server` | `shell_safe_allowlist` | `shell_dynamic_no_shell` and `benchmark_semantics` detect dynamic subprocess and pexpect execution |
8
+ | MCP002 | `filesystem_unrestricted` | `sensitive_read_only` | `filesystem_restricted` | `filesystem_guard_after_read` rejects a late guard while generic `.replace()` calls remain non-filesystem operations |
9
9
  | MCP003 | `url_arbitrary` | fixed URL in demo baseline | `url_allowlisted` | hostname mention and guard-after-request remain findings |
10
10
  | MCP005 | `sensitive_to_network` | `sensitive_read_only` | constrained source-only server | root corpus proves independent MCP contexts are isolated |
11
11
  | MCP016 | network expansion diff | unchanged diff | allowlist-added demo resolves risk | a new high-impact tool is an escalation |
@@ -16,3 +16,5 @@ MCP004, MCP007, and MCP010 also have paired approved/bounded and unapproved/unbo
16
16
  `postgresql_read_only` protects the semantic classifier from matching `post` inside `PostgreSQL`; `sql_destructive` proves that actual `DELETE`/`DROP` semantics still produce MCP004 and MCP010 with relevant evidence.
17
17
 
18
18
  Discovery is separately benchmarked by `fastmcp_async_annotations`, which must find both plain async `@mcp.tool()` and `@mcp.tool(annotations=ToolAnnotations(...))` decorators. Missing targets, unreadable trees, parse failures, and unexpected zero-tool scans fail closed.
19
+
20
+ Real-world regression fixtures also cover imported `mcp.tool()(function)` registration, helper-based path containment, SQLite reads, local file modes, local directory creation, `pexpect.spawn`, typed `Path.replace`, and ordinary string/datetime `.replace()` calls. MCP005 requires an actual outbound network destination; local writes and semantic-only action names cannot become exfiltration sinks. Read/list/status tools and incidental destructive words in documentation are negative cases for side-effect inference.
@@ -19,7 +19,7 @@ Install the wheel into a clean environment and verify `mcp-audit --version`, a p
19
19
 
20
20
  ## Publish
21
21
 
22
- 1. Create and push the matching Git tag, such as `v0.1.1`.
22
+ 1. Create and push the matching Git tag, such as `v0.1.3`.
23
23
  2. Publish the matching GitHub Release. This automatically runs `release.yml`.
24
24
  3. Verify `pipx install mcp-capdiff` in a clean environment.
25
25
  4. Run the tagged Action from the demo repository and confirm SARIF appears in Code Scanning.
@@ -4,7 +4,7 @@ MCP Audit maintains four independent versions:
4
4
 
5
5
  | Contract | Current | Change policy |
6
6
  | --- | --- | --- |
7
- | CLI/package | `0.1.1` | Semantic versioning; pre-1.0 minor releases may change CLI behavior |
7
+ | CLI/package | `0.1.3` | Semantic versioning; pre-1.0 minor releases may change CLI behavior |
8
8
  | Ruleset | `0.1` | Minor adds or materially changes rules; patch corrects implementation without intended semantic change |
9
9
  | JSON report schema | `1.0` | Minor changes are additive; major changes may remove fields or change meaning |
10
10
  | Manifest schema | `1.0` | Minor changes are additive; major changes may remove fields or change meaning |
@@ -0,0 +1,88 @@
1
+ from datetime import datetime, timezone
2
+ from pathlib import Path
3
+
4
+ import os
5
+ import pexpect
6
+ from fastmcp import FastMCP
7
+
8
+
9
+ mcp = FastMCP("benchmark-semantics")
10
+
11
+
12
+ @mcp.tool()
13
+ def list_tables(description: str) -> str:
14
+ """List database tables without modifying them."""
15
+ return description.replace("|", "\\|")
16
+
17
+
18
+ @mcp.tool()
19
+ def get_message_time() -> str:
20
+ """Read the current message timestamp."""
21
+ return datetime.now(timezone.utc).replace(tzinfo=None).isoformat()
22
+
23
+
24
+ @mcp.tool()
25
+ def spawn_process(command: str) -> str:
26
+ """Spawn an interactive command."""
27
+ return str(pexpect.spawn(command))
28
+
29
+
30
+ @mcp.tool()
31
+ def spawn_unicode_process(command: str) -> str:
32
+ """Spawn an interactive Unicode command."""
33
+ return str(pexpect.spawnu(command))
34
+
35
+
36
+ @mcp.tool()
37
+ def get_vacuum_analyze_stats() -> str:
38
+ """Retrieve read-only delete and revoke statistics; does not execute destructive operations."""
39
+ return "stats"
40
+
41
+
42
+ @mcp.tool(name="Type")
43
+ def type_text(text: str) -> str:
44
+ """Type text into the GUI. Clear existing content first with Ctrl+A, Delete."""
45
+ return text
46
+
47
+
48
+ @mcp.tool()
49
+ def list_email_tags() -> list[str]:
50
+ """List email tags without changing them."""
51
+ return []
52
+
53
+
54
+ @mcp.tool()
55
+ def delete_comment(comment_id: str) -> str:
56
+ """Delete a remote comment."""
57
+ return comment_id
58
+
59
+
60
+ @mcp.tool()
61
+ def send_email(message: str) -> str:
62
+ """Send an email message."""
63
+ return message
64
+
65
+
66
+ @mcp.tool()
67
+ def send_status(status: str) -> str:
68
+ """Send a status notification."""
69
+ return status
70
+
71
+
72
+ @mcp.tool()
73
+ def delete_history(history_id: str) -> str:
74
+ """Delete remote history."""
75
+ return history_id
76
+
77
+
78
+ @mcp.tool()
79
+ def replace_path(source: str, destination: str) -> str:
80
+ path = Path(source)
81
+ path.replace(destination)
82
+ return destination
83
+
84
+
85
+ @mcp.tool()
86
+ def replace_path_with_os(source: str, destination: str) -> str:
87
+ os.replace(source, destination)
88
+ return destination
@@ -0,0 +1,32 @@
1
+ from pathlib import Path
2
+
3
+ from fastmcp import FastMCP
4
+
5
+
6
+ ROOT = Path("vault").resolve()
7
+ mcp = FastMCP("filesystem-helper-restricted")
8
+
9
+
10
+ def _resolve(relative_path: str) -> Path:
11
+ candidate = (ROOT / relative_path).resolve()
12
+ try:
13
+ candidate.relative_to(ROOT)
14
+ except ValueError as exc:
15
+ raise ValueError("path escapes the approved root") from exc
16
+ return candidate
17
+
18
+
19
+ @mcp.tool()
20
+ def read_note(path: str) -> str:
21
+ return _resolve(path).read_text(encoding="utf-8")
22
+
23
+
24
+ @mcp.tool()
25
+ def search_content(folder: str) -> list[str]:
26
+ base = _resolve(folder)
27
+ return [path.read_text(encoding="utf-8") for path in base.rglob("*.md")]
28
+
29
+
30
+ @mcp.tool()
31
+ def delete_note(path: str) -> None:
32
+ _resolve(path).unlink()
@@ -0,0 +1,31 @@
1
+ import os
2
+
3
+ from fastmcp import FastMCP
4
+
5
+
6
+ mcp = FastMCP("local-filesystem-operations")
7
+
8
+
9
+ @mcp.tool()
10
+ def read_file(file: str) -> str:
11
+ with open(file, "r") as handle:
12
+ return handle.read()
13
+
14
+
15
+ @mcp.tool()
16
+ def write_file(file: str, content: str) -> str:
17
+ with open(file, "w") as handle:
18
+ handle.write(content)
19
+ return file
20
+
21
+
22
+ @mcp.tool()
23
+ def create_folder(folder: str) -> str:
24
+ os.makedirs(folder)
25
+ return folder
26
+
27
+
28
+ @mcp.tool()
29
+ def send_message(message: str) -> str:
30
+ """Prepare a message locally without any network destination."""
31
+ return message
@@ -0,0 +1 @@
1
+ """Functions registered as MCP tools by the fixture server."""
@@ -0,0 +1,23 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Literal
4
+
5
+
6
+ ALLOWED_ROOTS = [str(Path("data").resolve())]
7
+
8
+
9
+ def _validate_path(path: str) -> str:
10
+ resolved = os.path.realpath(path)
11
+ for root in ALLOWED_ROOTS:
12
+ prefix = root if root.endswith(os.sep) else root + os.sep
13
+ if resolved == root or resolved.startswith(prefix):
14
+ return resolved
15
+ raise ValueError("path is outside allowed roots")
16
+
17
+
18
+ def read_write_file(path: str, operation: Literal["read", "write"], content: str = "") -> str:
19
+ resolved = _validate_path(path)
20
+ if operation == "read":
21
+ return Path(resolved).read_text(encoding="utf-8")
22
+ Path(resolved).write_text(content, encoding="utf-8")
23
+ return resolved
@@ -0,0 +1,3 @@
1
+ def get_state() -> dict:
2
+ """Return read-only server state."""
3
+ return {"status": "ready"}
@@ -0,0 +1,10 @@
1
+ from fastmcp import FastMCP
2
+
3
+ from fixture_tools.filesystem import read_write_file
4
+ from fixture_tools.state import get_state
5
+
6
+
7
+ mcp = FastMCP("programmatic-registration")
8
+
9
+ mcp.tool()(read_write_file)
10
+ mcp.tool(name="state")(get_state)
@@ -0,0 +1,18 @@
1
+ from fastmcp import FastMCP
2
+
3
+
4
+ mcp = FastMCP("sqlite-read-only")
5
+
6
+
7
+ @mcp.tool()
8
+ def read_query(query: str) -> list[dict]:
9
+ """Execute a read-only query on the Messages database."""
10
+ cursor.execute(query)
11
+ return cursor.fetchall()
12
+
13
+
14
+ @mcp.tool()
15
+ def list_tables() -> list[str]:
16
+ """List tables from the Messages database."""
17
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
18
+ return [row[0] for row in cursor.fetchall()]
@@ -1,6 +1,6 @@
1
1
  """MCP Audit public package and independently evolving contract versions."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.3"
4
4
  RULESET_VERSION = "0.1"
5
5
  REPORT_SCHEMA_VERSION = "1.0"
6
6
  MANIFEST_SCHEMA_VERSION = "1.0"
@@ -115,11 +115,7 @@ def _tool_findings(tool: Tool) -> list[Finding]:
115
115
  def _cross_tool_findings(tools: list[Tool]) -> list[Finding]:
116
116
  findings: list[Finding] = []
117
117
  sensitive_sources = [tool for tool in tools if tool.capability.data_access and tool.capability.sensitivity != "public"]
118
- external_sinks = [
119
- tool
120
- for tool in tools
121
- if tool.capability.network == "unrestricted_outbound" or tool.capability.side_effect in {"external_write", "public_action"}
122
- ]
118
+ external_sinks = [tool for tool in tools if _has_external_destination(tool)]
123
119
  for source in sensitive_sources:
124
120
  for sink in external_sinks:
125
121
  if source.name == sink.name or source.context != sink.context:
@@ -152,12 +148,17 @@ def _cross_tool_findings(tools: list[Tool]) -> list[Finding]:
152
148
  ],
153
149
  path=[source.name, "agent_context", sink.name],
154
150
  data_classification=data_classes,
155
- destination=sink.capability.network_destination or sink.capability.network,
151
+ destination=sink.capability.network_destination,
156
152
  )
157
153
  )
158
154
  return findings
159
155
 
160
156
 
157
+ def _has_external_destination(tool: Tool) -> bool:
158
+ destination = tool.capability.network_destination
159
+ return tool.capability.network != "none" and bool(destination and destination != "none")
160
+
161
+
161
162
  def _evidence(tool: Tool, kinds: set[str]) -> list[Evidence]:
162
163
  return [item for item in tool.evidence if item.kind in kinds]
163
164