Use with caution. Address findings before production.
Scanned 6/7/2026, 7:05:35 AMยทCached resultยทFast Scanยท48 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
0
high
57
medium
59
low
MCP Server Information
Findings
This package receives a C grade with a safety score of 58/100, driven primarily by 57 medium-severity findings concentrated in readiness issues and resource exhaustion vulnerabilities. The 59 low-severity findings across readiness and resource exhaustion categories suggest the server lacks proper safeguards against denial-of-service attacks and may not be production-ready. You should address the readiness gaps and resource limits before deploying this in a shared or untrusted environment.
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=
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | FROM python:3.11-slim |
| 2 | |
| 3 | WORKDIR /app |
| 4 | |
| 5 | COPY requirements.txt . |
| 6 | RUN pip install --no-cache-dir -r requirements.txt |
| 7 | |
| 8 | COPY . . |
| 9 | |
| 10 | RUN chmod +x start.sh |
| 11 | |
| 12 | ENV PYTHONUNBUFFERED=1 |
| 13 | |
| 14 | # start.sh runs dashboard (8101) + MCP server (8000) |
| 15 | CMD ["./start.sh"] |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
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
| 601 | """ |
| 602 | try: |
| 603 | import jellyfish |
| 604 | return jellyfish.damerau_levenshtein_distance(s1, s2) |
| 605 | except ImportError: |
| 606 | pass |
| 607 | |
| 608 | # Pure Python fallback |
| 609 | len1, len2 = len(s1), len(s2) |
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
| 107 | for val in results: |
| 108 | if val is not None: |
| 109 | try: |
| 110 | total += int(val) |
| 111 | except (ValueError, TypeError): |
| 112 | pass |
| 113 | return total |
| 114 | except Exception as exc: |
| 115 | log.warning("daily_digest: error reading call counters โ %s", 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
| 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
| 183 | count += 1 |
| 184 | if count % batch_size == 0: |
| 185 | try: |
| 186 | pipe.execute() |
| 187 | except Exception: |
| 188 | pass |
| 189 | pipe = r.pipeline(transaction=False) |
| 190 | |
| 191 | try: |
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
| 429 | if count % batch_size == 0: |
| 430 | try: |
| 431 | pipe.execute() |
| 432 | except Exception: |
| 433 | pass |
| 434 | pipe = r.pipeline(transaction=False) |
| 435 | |
| 436 | try: |
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.