Use with caution. Address findings before production.
Scanned 6/8/2026, 3:37:59 AMยทCached resultยทFast Scanยท48 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
0
high
56
medium
54
low
MCP Server Information
Findings
This package receives a C grade with a safety score of 58/100, driven primarily by 56 medium-severity issues concentrated in readiness and resource exhaustion concerns. The readiness gaps (54 findings) suggest incomplete implementation or missing safeguards, while resource exhaustion risks (22 findings) indicate potential vulnerability to denial-of-service attacks or uncontrolled resource consumption. You should address these medium-severity issues before deployment, particularly around server configuration and error handling, though the absence of critical or high-severity findings means it's not inherently unsafe.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 20 findings using rule-based analysis. Upgrade for LLM consensus across 5 judges, AI-generated remediation, and cross-file taint analysis.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
20 of 20 findings
20 findings
MCP server holds a mutable shared container (cache / store / state / pool / registry / sessions / results / outputs) and mutates it via append / push / add / extend, but no caller-identity marker (user_id / session_id / caller_id / request_id / org_id / tenant_id / actor_id / subject / principal) appears anywhere in the file. A process-global list mutated from inside a tool handler with no caller partition leaks data across requests: a later caller can read what an earlier caller wrote. Closes
Evidence
| 1 | """ |
| 2 | datanexus/tools/t11.py โ T11 Global Patent Intelligence tool. |
| 3 | |
| 4 | Spec: DataNexus_MCP_Spec_v7_4.docx Section 4, T11 entry |
| 5 | |
| 6 | Exactly 4 data functions. Shared infrastructure tools (report_feedback, |
| 7 | report_mcpize_link) are registered ONCE in main.py โ NOT here. |
| 8 | |
| 9 | Data sources: |
| 10 | Primary: EPO OPS (Open Patent Services) โ ops.epo.org โ OAuth client_credentials |
| 11 | European Patent Office API; 4 GB/month free tier |
| 12 | Secondary: USPTO PatentsView โ api.patentsview.org โ no key required |
| 13 | |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server file uses Python's `global` keyword. `global` mutates module-level state from inside a function โ in a multi-tenant MCP server, this is almost always a cross-request data path. Closes the OWASP MCP Top 10:2025 MCP10 (Context Injection & Over-Sharing) gap. Fix: thread state through function arguments or a per-request context object. Module-level mutable singletons have no place in a tool handler.
Evidence
| 97 | def _set_redis_client(client: Optional[redis_lib.Redis]) -> None: |
| 98 | """Inject a Redis client โ used in tests (e.g. fakeredis). Pass None to reset.""" |
| 99 | global _redis_client |
| 100 | _redis_client = client |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server file uses Python's `global` keyword. `global` mutates module-level state from inside a function โ in a multi-tenant MCP server, this is almost always a cross-request data path. Closes the OWASP MCP Top 10:2025 MCP10 (Context Injection & Over-Sharing) gap. Fix: thread state through function arguments or a per-request context object. Module-level mutable singletons have no place in a tool handler.
Evidence
| 52 | def _get_frontend_corpus() -> list: |
| 53 | global _FRONTEND_CORPUS |
| 54 | if _FRONTEND_CORPUS is None: |
| 55 | try: |
| 56 | with open(_CORPUS_PATH) as f: |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server file uses Python's `global` keyword. `global` mutates module-level state from inside a function โ in a multi-tenant MCP server, this is almost always a cross-request data path. Closes the OWASP MCP Top 10:2025 MCP10 (Context Injection & Over-Sharing) gap. Fix: thread state through function arguments or a per-request context object. Module-level mutable singletons have no place in a tool handler.
Evidence
| 71 | def _get_redis() -> Optional[redis_lib.Redis]: |
| 72 | """Lazy Redis connection. Returns None if unavailable โ never raises.""" |
| 73 | global _redis_client |
| 74 | if _redis_client is not None: |
| 75 | return _redis_client |
| 76 | try: |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server holds a mutable shared container (cache / store / state / pool / registry / sessions / results / outputs) and mutates it via append / push / add / extend, but no caller-identity marker (user_id / session_id / caller_id / request_id / org_id / tenant_id / actor_id / subject / principal) appears anywhere in the file. A process-global list mutated from inside a tool handler with no caller partition leaks data across requests: a later caller can read what an earlier caller wrote. Closes
Evidence
| 1 | """ |
| 2 | datanexus/tools/t10.py โ T10 OSS Dependency & Vulnerability Intelligence. |
| 3 | |
| 4 | Spec: DataNexus_MCP_Spec_v7_3.docx Section 11.3 / Table 140 (authoritative) |
| 5 | |
| 6 | Exactly 5 data functions + 2 infrastructure stubs = 7 total. |
| 7 | |
| 8 | Data sources: |
| 9 | Primary: Google OSV.dev API (api.osv.dev/v1) โ Apache 2.0, no key. |
| 10 | Secondary: NIST NVD CVE API (services.nvd.nist.gov) โ public domain, no key. |
| 11 | Supporting: deps.dev API (api.deps.dev/v3alpha) โ Apache 2.0, no key. |
| 12 | |
| 13 | Hard stops (Section 12.5): |
| 14 | - NEVER return |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server file uses Python's `global` keyword. `global` mutates module-level state from inside a function โ in a multi-tenant MCP server, this is almost always a cross-request data path. Closes the OWASP MCP Top 10:2025 MCP10 (Context Injection & Over-Sharing) gap. Fix: thread state through function arguments or a per-request context object. Module-level mutable singletons have no place in a tool handler.
Evidence
| 59 | def _set_redis_client(client: Optional[redis_lib.Redis]) -> None: |
| 60 | """Inject a Redis client (e.g. fakeredis) for testing. Pass None to reset.""" |
| 61 | global _redis_client |
| 62 | _redis_client = client |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server holds a mutable shared container (cache / store / state / pool / registry / sessions / results / outputs) and mutates it via append / push / add / extend, but no caller-identity marker (user_id / session_id / caller_id / request_id / org_id / tenant_id / actor_id / subject / principal) appears anywhere in the file. A process-global list mutated from inside a tool handler with no caller partition leaks data across requests: a later caller can read what an earlier caller wrote. Closes
Evidence
| 1 | """ |
| 2 | datanexus/tools/nonprofit_sprint7.py โ Sprint 7 nonprofit depth tools. |
| 3 | |
| 4 | Tools: |
| 5 | search_nonprofits_by_category โ search US nonprofits by NTEE category + state |
| 6 | fetch_nonprofit_financial_trends โ multi-year revenue/expense/asset trends for a nonprofit |
| 7 | |
| 8 | OQ1 resolved: ProPublica /api/v2/organizations/{ein}.json returns pre-computed |
| 9 | fields (totrevenue, totfuncexpns, totprgmrevnue, netassetsend, tax_prd_yr) in |
| 10 | filings_with_data. No raw 990 JSON parsing needed. |
| 11 | |
| 12 | Circuit breaker: _propublica_br |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server holds a mutable shared container (cache / store / state / pool / registry / sessions / results / outputs) and mutates it via append / push / add / extend, but no caller-identity marker (user_id / session_id / caller_id / request_id / org_id / tenant_id / actor_id / subject / principal) appears anywhere in the file. A process-global list mutated from inside a tool handler with no caller partition leaks data across requests: a later caller can read what an earlier caller wrote. Closes
Evidence
| 1 | """ |
| 2 | datanexus/tools/security_sprint6.py โ Sprint 6 security tools. |
| 3 | |
| 4 | Tools: |
| 5 | fetch_package_maintainer_history โ maintainer health + anomaly score |
| 6 | fetch_package_risk_brief โ SHIP/CAUTION/BLOCK aggregator |
| 7 | detect_typosquatting โ Damerau-Levenshtein vs top-10k (added in final step) |
| 8 | |
| 9 | All are thin MCP wrappers. Logic lives in _security_utils.py / _maintainer_utils.py. |
| 10 | HTTP self-calls are forbidden โ utilities are called directly. |
| 11 | """ |
| 12 | |
| 13 | import asyncio |
| 14 | import logging |
| 15 | import tim |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server file uses Python's `global` keyword. `global` mutates module-level state from inside a function โ in a multi-tenant MCP server, this is almost always a cross-request data path. Closes the OWASP MCP Top 10:2025 MCP10 (Context Injection & Over-Sharing) gap. Fix: thread state through function arguments or a per-request context object. Module-level mutable singletons have no place in a tool handler.
Evidence
| 67 | connection is attempted rather than returning a broken client whose write errors |
| 68 | would be silently swallowed by the outer try/except in report_feedback(). |
| 69 | """ |
| 70 | global _redis_client |
| 71 | if _redis_client is not None: |
| 72 | try: |
| 73 | _redis_client.ping() |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server file uses Python's `global` keyword. `global` mutates module-level state from inside a function โ in a multi-tenant MCP server, this is almost always a cross-request data path. Closes the OWASP MCP Top 10:2025 MCP10 (Context Injection & Over-Sharing) gap. Fix: thread state through function arguments or a per-request context object. Module-level mutable singletons have no place in a tool handler.
Evidence
| 65 | def _set_caller_id(caller_id: Optional[str]) -> None: |
| 66 | """Inject a fixed caller_id for testing. Pass None to reset.""" |
| 67 | global _test_caller_id |
| 68 | _test_caller_id = caller_id |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server holds a mutable shared container (cache / store / state / pool / registry / sessions / results / outputs) and mutates it via append / push / add / extend, but no caller-identity marker (user_id / session_id / caller_id / request_id / org_id / tenant_id / actor_id / subject / principal) appears anywhere in the file. A process-global list mutated from inside a tool handler with no caller partition leaks data across requests: a later caller can read what an earlier caller wrote. Closes
Evidence
| 1 | """ |
| 2 | datanexus/tools/t04.py โ T04 IRS 990 / Nonprofit Data tool. |
| 3 | |
| 4 | Spec: DataNexus_MCP_Spec_v7_3.docx Section 12.4 / Phase 2 Step B |
| 5 | |
| 6 | Exactly 3 data functions + 2 infrastructure stubs = 5 total. |
| 7 | (Section 11.3, Table 163 โ authoritative signatures) |
| 8 | |
| 9 | Data sources: |
| 10 | Primary: IRS EO BMF (irs.gov) โ public domain |
| 11 | Secondary: IRS TEOS bulk downloads (irs.gov) โ public domain |
| 12 | Tertiary: UK Charity Commission public bulk extract (no auth required) |
| 13 | ccewuksprdoneregsadata1.blob.core.windo |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP server file uses Python's `global` keyword. `global` mutates module-level state from inside a function โ in a multi-tenant MCP server, this is almost always a cross-request data path. Closes the OWASP MCP Top 10:2025 MCP10 (Context Injection & Over-Sharing) gap. Fix: thread state through function arguments or a per-request context object. Module-level mutable singletons have no place in a tool handler.
Evidence
| 58 | def _get_redis() -> Optional[redis_lib.Redis]: |
| 59 | """Lazy Redis connection. Returns None if unavailable โ never raises.""" |
| 60 | global _redis_client |
| 61 | if _redis_client is not None: |
| 62 | return _redis_client |
| 63 | try: |
Remediation
Partition shared state by caller identity, or eliminate it. Wrong: _cache = {} @mcp.tool() def search(query: str) -> list: if query in _cache: return _cache[query] result = _expensive(query) _cache[query] = result return result Right (key by caller): _cache: dict[str, dict] = {} @mcp.tool() def search(query: str, ctx) -> list: user_cache = _cache.setdefault(ctx.user_id, {}) if query in user_cache:
MCP tool file registers a tool, performs a destructive sink (fs.unlink / shutil.rmtree / DROP TABLE / DELETE FROM / TRUNCATE / UPDATE ... SET / HTTP DELETE|PUT|PATCH / subprocess / exec / spawn), and emits no audit event anywhere in the file. Without an audit event, an investigator cannot answer "who deleted record X on day Y?" โ the irreversible action leaves no trail. Closes the OWASP MCP Top 10:2025 MCP08 (Lack of Audit and Telemetry) gap. Distinct from MCP-201 (no confirmation) and MCP-283
Evidence
| 1 | """ |
| 2 | datanexus/tools/security_stateful.py โ Sprint 6 stateful security tools. |
| 3 | |
| 4 | Tools: |
| 5 | fetch_cve_watch โ persistent CVE watchlist (create/check/delete) |
| 6 | audit_sbom_continuous โ continuous SBOM monitoring (register/check/deregister) |
| 7 | |
| 8 | Redis key schema (dn: prefix): |
| 9 | dn:cve_watch:{watch_id} โ Hash (watch data) |
| 10 | dn:cve_watch_ids โ SET (watch index) |
| 11 | dn:sbom_watch:{watch_id} โ Hash (SBOM watch data) |
| 12 | dn:sbom_watch_ids โ SET (SBOM watch index) |
| 13 | |
| 14 | Scheduler: see datanex |
Remediation
Add a structured audit-event emit immediately after every destructive sink. Minimum schema: actor, action, target, outcome, request_id. Python: from acme.audit import audit_log @mcp.tool() def delete_record(token: str, record_id: str) -> dict: actor = verify_token(token) db.execute("DELETE FROM records WHERE id = %s", (record_id,)) audit_log( actor=actor.sub, action="delete_record", target=record_id, outcome=
MCP tool file registers a tool, performs a destructive sink (fs.unlink / shutil.rmtree / DROP TABLE / DELETE FROM / TRUNCATE / UPDATE ... SET / HTTP DELETE|PUT|PATCH / subprocess / exec / spawn), and emits no audit event anywhere in the file. Without an audit event, an investigator cannot answer "who deleted record X on day Y?" โ the irreversible action leaves no trail. Closes the OWASP MCP Top 10:2025 MCP08 (Lack of Audit and Telemetry) gap. Distinct from MCP-201 (no confirmation) and MCP-283
Evidence
| 1 | """ |
| 2 | datanexus/tools/api_key_sprint8a.py โ Sprint 8A |
| 3 | |
| 4 | Three API key management tools + _UsageMiddleware. |
| 5 | |
| 6 | Tools (sub-server: datanexus-apikeys): |
| 7 | generate_api_key โ issue a new dnx_... key, store email in DB |
| 8 | rotate_api_key โ revoke current key, issue replacement |
| 9 | revoke_api_key โ mark key revoked, invalidate Redis cache |
| 10 | |
| 11 | _UsageMiddleware (FastMCP Middleware subclass): |
| 12 | Intercepts every tool/call, increments per-user monthly Redis counter, |
| 13 | injects usage fields into the response. Fai |
Remediation
Add a structured audit-event emit immediately after every destructive sink. Minimum schema: actor, action, target, outcome, request_id. Python: from acme.audit import audit_log @mcp.tool() def delete_record(token: str, record_id: str) -> dict: actor = verify_token(token) db.execute("DELETE FROM records WHERE id = %s", (record_id,)) audit_log( actor=actor.sub, action="delete_record", target=record_id, outcome=
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 83 | } |
| 84 | |
| 85 | # WRONG โ agent cannot reason about this |
| 86 | return {"error": str(e)} |
| 87 | ``` |
| 88 | |
| 89 | Agents that receive structured error codes can retry intelligently, surface the right message to users, |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 223 | try: |
| 224 | r = await get_redis() |
| 225 | if r: |
| 226 | await r.set(cache_key, tier, ex=300) |
| 227 | except Exception: |
| 228 | pass |
| 229 | return tier |
| 230 | |
| 231 | except Exception as exc: |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 99 | # remaining allowlist domains โ any path counts |
| 100 | for allowed in _PATCH_ALLOWLIST: |
| 101 | if host == allowed or host.endswith("." + allowed): |
| 102 | return True |
| 103 | except Exception: |
| 104 | pass |
| 105 | return False |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 1184 | raw = r.hget(key_feedback(tool_id, rid), "data") |
| 1185 | if raw: |
| 1186 | try: |
| 1187 | records.append(json.loads(raw)) |
| 1188 | except Exception: |
| 1189 | pass |
| 1190 | except Exception: |
| 1191 | pass |
| 1192 | results[tool_id] = records |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 59 | try: |
| 60 | r = _get_redis() |
| 61 | if r: |
| 62 | r.set(LAST_ERROR_KEY, msg[:500], ex=7 * 86400) |
| 63 | except Exception: |
| 64 | pass |
| 65 | |
| 66 | |
| 67 | async def refresh() -> bool: |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 1186 | try: |
| 1187 | records.append(json.loads(raw)) |
| 1188 | except Exception: |
| 1189 | pass |
| 1190 | except Exception: |
| 1191 | pass |
| 1192 | results[tool_id] = records |
| 1193 | |
| 1194 | return JSONResponse({"feedback": results}) |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.