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.
- mcp_capdiff-0.1.3/CHANGELOG.md +73 -0
- {mcp_capdiff-0.1.1/src/mcp_capdiff.egg-info → mcp_capdiff-0.1.3}/PKG-INFO +1 -1
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/precision-matrix.md +4 -2
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/releasing.md +1 -1
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/versioning.md +1 -1
- mcp_capdiff-0.1.3/fixtures/benchmark_semantics/server.py +88 -0
- mcp_capdiff-0.1.3/fixtures/filesystem_helper_restricted/server.py +32 -0
- mcp_capdiff-0.1.3/fixtures/local_filesystem_operations/server.py +31 -0
- mcp_capdiff-0.1.3/fixtures/programmatic_registration/fixture_tools/__init__.py +1 -0
- mcp_capdiff-0.1.3/fixtures/programmatic_registration/fixture_tools/filesystem.py +23 -0
- mcp_capdiff-0.1.3/fixtures/programmatic_registration/fixture_tools/state.py +3 -0
- mcp_capdiff-0.1.3/fixtures/programmatic_registration/server.py +10 -0
- mcp_capdiff-0.1.3/fixtures/sqlite_read_only/server.py +18 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/__init__.py +1 -1
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/rules/engine.py +7 -6
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/scanners/source/python.py +382 -31
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3/src/mcp_capdiff.egg-info}/PKG-INFO +1 -1
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/SOURCES.txt +9 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/scan.json +1 -1
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/scan.sarif.json +1 -1
- mcp_capdiff-0.1.3/tests/test_real_world_regressions.py +92 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_reporters.py +2 -2
- mcp_capdiff-0.1.1/CHANGELOG.md +0 -40
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/LICENSE +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/MANIFEST.in +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/README.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/SECURITY.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/action.yml +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/policy.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/report-schema.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP001.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP002.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP003.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP004.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP005.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP007.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP010.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP016.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/MCP017.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/rules/README.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/docs/v0.1-scope.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/.github/workflows/mcp-audit.yml +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/README.md +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/mcp-audit.yaml +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/pyproject.toml +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/approval_removed/after.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/approval_removed/before.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/filesystem_expansion/after.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/filesystem_expansion/before.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/network_allowlist_added/after.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/network_allowlist_added/before.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/unrestricted_fetch/after.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/examples/vulnerable-fastmcp/scenarios/unrestricted_fetch/before.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/destructive_with_approval/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/destructive_without_approval/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/fastmcp_async_annotations/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/filesystem_guard_after_read/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/filesystem_restricted/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/filesystem_unrestricted/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/postgresql_read_only/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/safe_server/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/sensitive_read_only/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/sensitive_to_network/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/shell_direct/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/shell_dynamic_no_shell/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/shell_safe_allowlist/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/sql_destructive/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/url_allowlisted/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/url_arbitrary/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/url_guard_after_request/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/url_hostname_unguarded/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/fixtures/vulnerable_server/server.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/mcp-audit.yaml +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/pyproject.toml +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/setup.cfg +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/__main__.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/analysis/__init__.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/analysis/diff.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/analysis/scan.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/cli/__init__.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/cli/main.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/models/__init__.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/models/capability.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/models/finding.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/models/report.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/policy/__init__.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/policy/evaluator.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/policy/loader.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/__init__.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/json.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/manifest.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/sarif.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/reporters/terminal.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/rules/__init__.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/scanners/__init__.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_audit/scanners/source/__init__.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/dependency_links.txt +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/entry_points.txt +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/requires.txt +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/src/mcp_capdiff.egg-info/top_level.txt +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/diff.txt +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/manifest.yaml +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/golden/terminal.txt +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_demo_scenarios.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_diff.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_discovery.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_golden.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_policy.py +0 -0
- {mcp_capdiff-0.1.1 → mcp_capdiff-0.1.3}/tests/test_rule_accuracy.py +0 -0
- {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.
|
|
@@ -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`
|
|
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.
|
|
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.
|
|
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,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()]
|
|
@@ -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
|
|
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
|
|