High risk. Don't ship without significant remediation.
Scanned 6/8/2026, 5:12:04 AMยทCached resultยทDeep Scanยท91 rulesยทHow we decide โ
AIVSS Score
High
Severity Breakdown
0
critical
32
high
53
medium
53
low
MCP Server Information
Findings
This package has a D security grade with a safety score of 44/100, driven primarily by 53 medium-severity readiness issues and 35 server configuration problems that could leave it vulnerable in production environments. The 32 high-severity findings include 12 data exfiltration risks, 9 prompt injection vulnerabilities, and 18 resource exhaustion issues that could be exploited to compromise data or degrade service. Installation is not recommended without significant remediation of these configuration and architectural gaps.
AIPer-finding remediation generated by bedrock-claude-haiku-4-5 โ 40 of 50 findings. Click any finding to read.
No known CVEs found for this package or its dependencies.
Scan Details
Done
Sign in to save scan history and re-scan automatically on new commits.
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.
Showing 1โ30 of 50 findings
50 findings
Tool 'search_datanexus_tools' contains imperative instruction directed at LLM: 'Use this first' attempts to influence routing priority.
RemediationAI
The problem is that the tool description contains an imperative instruction ('Use this first') that attempts to influence LLM routing priority, violating the principle that tool manifests should be declarative and neutral. Remove the phrase 'Use this first' and any other imperative language from the tool description in mcp-manifest.json. Replace it with a neutral, factual description of what the tool does (e.g., 'Searches DataNexus tools by keyword'). This ensures the LLM makes routing decisions based on semantic relevance, not embedded instructions. Verify by reviewing the updated manifest and confirming no imperative verbs (use, prefer, prioritize, etc.) appear in tool descriptions.
The MCP proxy (datanexus-mcp.js) forwards all tool calls to a remote endpoint (https://datanexusmcp.com/mcp) at runtime, allowing the remote server to dynamically determine tool behavior, validation rules, and response formatting per invocation without the manifest reflecting these changes.
Evidence
| 1 | #!/usr/bin/env node |
| 2 | /** |
| 3 | * DataNexus MCP โ stdio proxy for Glama and local MCP clients. |
| 4 | * |
| 5 | * Bridges stdio โ the live remote server at https://datanexusmcp.com/mcp |
| 6 | * using mcp-remote. No Python, no Redis, no PostgreSQL required. |
| 7 | * |
| 8 | * Usage: |
| 9 | * npx -y @datanexusmcp/mcp-server |
| 10 | * |
| 11 | * Claude Desktop claude_desktop_config.json: |
| 12 | * { |
| 13 | * "mcpServers": { |
| 14 | * "datanexus": { |
| 15 | * "command": "npx", |
| 16 | * "args": ["-y", "@datanexusmcp/mcp-server"] |
| 17 | * } |
| 18 | * } |
| 19 | * } |
| 20 | */ |
| 21 | |
| 22 | c |
RemediationAI
The problem is that datanexus-mcp.js acts as a transparent proxy forwarding all tool calls to a remote endpoint (https://datanexusmcp.com/mcp) without any local validation, allowing the remote server to dynamically determine tool behavior, validation rules, and response formatting at runtimeโcreating an unauditable supply-chain risk. Replace the remote proxy pattern with local tool implementations: remove the mcp-remote forwarding logic and implement tool handlers directly in the MCP server process, or if remote calls are necessary, add a local validation layer that verifies responses against a schema defined in the manifest before returning to the client. This ensures all tool behavior is deterministic, auditable, and matches the declared manifest. Verify by running the MCP server locally, calling a tool, and confirming the response matches the schema declared in mcp-manifest.json without any remote server involvement.
Tool 'search_datanexus_tools' accepts natural language queries that are passed to a remote MCP server (https://datanexusmcp.com/mcp) which determines tool routing and behavior dynamically based on query content at call-time.
RemediationAI
The problem is that the 'search_datanexus_tools' tool accepts natural language queries that are passed to a remote MCP server which determines tool routing and behavior dynamically based on query content at call-time, creating runtime behavior that is not reflected in the manifest and cannot be audited. Implement local query parsing and tool routing logic in the MCP server instead of delegating to a remote endpoint; define all possible routing outcomes and tool selections in the manifest schema. If remote augmentation is needed, add a local caching layer that pre-fetches and validates remote tool definitions at startup, not at call-time. This ensures tool behavior is deterministic and matches the manifest. Verify by inspecting the tool handler code to confirm all routing decisions are made locally based on a fixed set of rules, and by calling the tool multiple times with the same query to confirm identical results.
Tool nonprofit_fetch_charity_uk accepts charity_number_or_name identifier and returns complete UK charity registration data (income, activities, trustees) by lookup alone without verifying caller authorization to access that charity's sensitive information.
RemediationAI
The problem is that the 'nonprofit_fetch_charity_uk' tool returns complete UK charity registration data (income, activities, trustees) based solely on a charity number or name lookup without verifying that the caller is authorized to access that charity's sensitive information. Add a caller authorization check before returning data: implement a function that validates the caller's identity (from the MCP request context) against an access control list or permission model, and return only the fields the caller is entitled to see (or reject the request with a 403 Forbidden error). This prevents unauthorized disclosure of sensitive nonprofit financial and governance data. Verify by calling the tool with a test caller ID that lacks permission and confirming the tool returns an authorization error; then call with an authorized caller ID and confirm full data is returned.
Tool nonprofit_fetch_nonprofit_by_ein accepts ein identifier and returns complete nonprofit record (revenue, expenses, assets, mission) by EIN lookup alone without verifying caller ownership or authorization to access that nonprofit's financial data.
RemediationAI
The problem is that the 'nonprofit_fetch_nonprofit_by_ein' tool returns complete nonprofit records (revenue, expenses, assets, mission) based solely on EIN lookup without verifying that the caller owns or is authorized to access that nonprofit's financial data. Add a caller authorization check: implement a function that validates the caller's identity against the nonprofit's ownership or a permission model, and return only permitted fields or reject with a 403 error. This prevents unauthorized access to sensitive financial and operational data. Verify by calling the tool with an unauthorized caller ID and confirming an authorization error is returned; then call with an authorized caller and confirming full data is returned.
Tool 'security_fetch_package_vulnerabilities' calls Google OSV.dev and NIST NVD using module-level NVD_API_KEY from config.py (os.environ) without consulting caller identity or per-request credentials.
RemediationAI
The problem is that the 'security_fetch_package_vulnerabilities' tool calls Google OSV.dev and NIST NVD using module-level NVD_API_KEY from config.py without consulting caller identity or per-request credentials, allowing all callers to share the same API quota and preventing per-caller rate limiting or audit trails. Modify the tool handler to accept an optional 'api_key' parameter in the tool input schema, and pass caller-provided credentials (or retrieve them from a per-request context object) to the upstream API calls instead of using a module-level key. If no per-request key is available, fall back to a shared key but log the caller identity alongside the API call. This enables per-caller quota management and audit trails. Verify by calling the tool twice with different caller IDs and confirming both the API call logs and the tool's response include the caller identity.
Tool 'nonprofit_search_nonprofits_by_name' calls IRS EO BMF upstream API using module-level IRS_EFTS_BASE URL from config.py without consulting caller identity or per-request credentials.
RemediationAI
The problem is that the 'nonprofit_search_nonprofits_by_name' tool calls the IRS EO BMF upstream API using a module-level IRS_EFTS_BASE URL from config.py without consulting caller identity or per-request credentials, preventing per-caller rate limiting and audit trails. Modify the tool handler to accept an optional 'caller_id' or 'api_key' parameter, and include it in the upstream API call headers or logs so that each request is attributed to the caller. If per-request credentials are not available, at minimum log the caller identity from the MCP request context alongside the API call. This enables audit trails and per-caller rate limiting. Verify by calling the tool twice with different caller IDs and confirming the server logs show both the caller identity and the upstream API call.
Tool 'security_fetch_dependency_graph' calls deps.dev (Google) using module-level credentials from config.py without consulting caller identity or per-request credentials.
RemediationAI
The problem is that the 'security_fetch_dependency_graph' tool calls deps.dev (Google) using module-level credentials from config.py without consulting caller identity or per-request credentials, preventing per-caller quota management and audit trails. Modify the tool handler to accept an optional 'api_key' or 'caller_id' parameter, and pass it to the upstream API call or include it in request headers and logs. This ensures each call is attributed to the caller and enables per-caller rate limiting. Verify by calling the tool twice with different caller IDs and confirming the server logs show both the caller identity and the upstream API call.
Tool 'nonprofit_fetch_nonprofit_by_ein' calls IRS EO BMF upstream API using module-level IRS_EFTS_BASE URL from config.py without consulting caller identity or per-request credentials.
RemediationAI
The problem is that the 'nonprofit_fetch_nonprofit_by_ein' tool calls the IRS EO BMF upstream API using a module-level IRS_EFTS_BASE URL from config.py without consulting caller identity or per-request credentials, preventing per-caller rate limiting and audit trails. Modify the tool handler to accept an optional 'caller_id' parameter and include it in the upstream API call headers or logs so that each request is attributed to the caller. This enables audit trails and per-caller rate limiting. Verify by calling the tool twice with different caller IDs and confirming the server logs show both the caller identity and the upstream API call.
Tool 'nonprofit_fetch_charity_uk' calls UK Charity Commission API using module-level UK_CHARITY_BASE URL from config.py without consulting caller identity or per-request credentials.
RemediationAI
The problem is that the 'nonprofit_fetch_charity_uk' tool calls the UK Charity Commission API using a module-level UK_CHARITY_BASE URL from config.py without consulting caller identity or per-request credentials, preventing per-caller rate limiting and audit trails. Modify the tool handler to accept an optional 'caller_id' parameter and include it in the upstream API call headers or logs so that each request is attributed to the caller. This enables audit trails and per-caller rate limiting. Verify by calling the tool twice with different caller IDs and confirming the server logs show both the caller identity and the upstream API call.
Tool 'security_fetch_package_vulnerabilities' returns CVE data from untrusted external sources (Google OSV.dev + NIST NVD) including free-text fields (severity descriptions, CVSS vectors) without provenance delimiters, enabling indirect prompt injection.
RemediationAI
The problem is that the 'security_fetch_package_vulnerabilities' tool returns CVE data from untrusted external sources (Google OSV.dev and NIST NVD) including free-text fields (severity descriptions, CVSS vectors) without provenance delimiters, enabling indirect prompt injection if the LLM processes these fields as instructions. Add provenance markers to all external data: wrap each free-text field with a clear delimiter (e.g., `[EXTERNAL_DATA_START] ... [EXTERNAL_DATA_END]`) and include the source URL and fetch timestamp, or sanitize free-text fields by removing special characters and limiting to alphanumeric + basic punctuation. This prevents injected instructions from being interpreted as tool directives. Verify by calling the tool with a package known to have CVE descriptions, and confirming the response includes provenance markers or sanitized text without special characters.
Tool 'security_fetch_dependency_graph' returns transitive dependency metadata from untrusted external source (deps.dev) without visible provenance markers, risking indirect prompt injection through package names and descriptions.
RemediationAI
The problem is that the 'security_fetch_dependency_graph' tool returns transitive dependency metadata from untrusted external source (deps.dev) without visible provenance markers, risking indirect prompt injection through package names and descriptions. Add provenance markers to all external data: wrap each free-text field with a clear delimiter (e.g., `[EXTERNAL_DATA_START] ... [EXTERNAL_DATA_END]`) and include the source URL and fetch timestamp, or sanitize free-text fields by removing special characters. This prevents injected instructions from being interpreted as tool directives. Verify by calling the tool with a package known to have descriptions, and confirming the response includes provenance markers or sanitized text.
feedback_classifier tool calls classify() which invokes Anthropic SDK without max_tokens cap
Evidence
| 1 | """ |
| 2 | datanexus/agents/feedback_classifier.py โ Haiku Trigger 2: feedback classification. |
| 3 | |
| 4 | Spec: DataNexus_MCP_Spec_v7_4.docx Section 13.4 / Phase 4 |
| 5 | |
| 6 | Most important file in Section 13. Closes the loop between user-reported |
| 7 | feedback and automated data quality triage. |
| 8 | |
| 9 | Rules (CLAUDE.md): |
| 10 | S13-1: This is Haiku Trigger T2. Delegate to haiku_classifier.classify() only. |
| 11 | S13-4: Never raises. All exceptions caught. Always returns structured result. |
| 12 | S13-5: FeedbackRecord.classification is ONE-WAY onl |
RemediationAI
The problem is that the feedback_classifier tool calls the classify() function which invokes the Anthropic SDK without a max_tokens cap, allowing unbounded token consumption and potential cost explosion or denial of service. Add a max_tokens parameter to the anthropic.messages.create() call in the classify() function: set it to a reasonable limit (e.g., 500 tokens for classification tasks) based on the expected output size. This caps the cost and latency of each classification call. Verify by calling the feedback_classifier tool and confirming the Anthropic API call includes max_tokens in the request; check the response to confirm it is truncated if it would exceed the limit.
schema_monitor tool calls classify() which invokes Anthropic SDK without max_tokens cap
Evidence
| 1 | """ |
| 2 | datanexus/agents/schema_monitor.py โ Haiku Trigger 3: schema change assessment. |
| 3 | |
| 4 | Spec: DataNexus_MCP_Spec_v7_4.docx Section 13.5 / Phase 5 |
| 5 | |
| 6 | Called when the ingest worker detects a structural change between the |
| 7 | previously-stored schema fingerprint and the newly-fetched payload shape. |
| 8 | Haiku decides whether the change is breaking and what severity to assign. |
| 9 | |
| 10 | Rules (CLAUDE.md): |
| 11 | S13-1: This is Haiku Trigger T3. Delegate to haiku_classifier.classify() only. |
| 12 | S13-4: Never raises. Always return |
RemediationAI
The problem is that the schema_monitor tool calls the classify() function which invokes the Anthropic SDK without a max_tokens cap, allowing unbounded token consumption and potential cost explosion or denial of service. Add a max_tokens parameter to the anthropic.messages.create() call in the classify() function: set it to a reasonable limit (e.g., 500 tokens) based on the expected output size. This caps the cost and latency of each classification call. Verify by calling the schema_monitor tool and confirming the Anthropic API call includes max_tokens in the request.
anomaly_reviewer tool calls classify() which invokes Anthropic SDK without max_tokens cap
Evidence
| 1 | """ |
| 2 | datanexus/agents/anomaly_reviewer.py โ Haiku Trigger 1: anomaly review. |
| 3 | |
| 4 | Spec: DataNexus_MCP_Spec_v7_4.docx Section 13.3 / Phase 3 |
| 5 | |
| 6 | Called when the deterministic validator (validator.py) flags an anomaly |
| 7 | that needs a second-opinion classification before deciding whether to |
| 8 | keep, suppress, or flag the affected record. |
| 9 | |
| 10 | Rules (CLAUDE.md): |
| 11 | S13-1: This is Haiku Trigger T1. Do NOT call Anthropic API directly โ |
| 12 | always delegate to haiku_classifier.classify(). |
| 13 | S13-4: Never raises. All |
RemediationAI
The problem is that the anomaly_reviewer tool calls the classify() function which invokes the Anthropic SDK without a max_tokens cap, allowing unbounded token consumption and potential cost explosion or denial of service. Add a max_tokens parameter to the anthropic.messages.create() call in the classify() function: set it to a reasonable limit (e.g., 500 tokens) based on the expected output size. This caps the cost and latency of each classification call. Verify by calling the anomaly_reviewer tool and confirming the Anthropic API call includes max_tokens in the request.
Tool handlers call Anthropic SDK via haiku_classifier.classify() without passing max_tokens parameter to anthropic.messages.create()
Evidence
| 1 | """ |
| 2 | datanexus/agents/haiku_classifier.py โ Shared Haiku API wrapper for all 4 triggers. |
| 3 | |
| 4 | Spec: DataNexus_MCP_Spec_v7_4.docx Section 13.2 / Phase 2 |
| 5 | |
| 6 | Rules (CLAUDE.md): |
| 7 | S13-1: Called ONLY from the 4 permitted triggers โ never directly from tool handlers. |
| 8 | S13-2: HAIKU_MODEL imported from feedback/config.py. Never hardcoded here. |
| 9 | S13-3: Daily cap HAIKU_MAX_CALLS_PER_DAY (100) enforced before every call. |
| 10 | incr โ expire โ check, in that exact order. |
| 11 | S13-4: On any API error: return err |
RemediationAI
The problem is that the haiku_classifier.classify() function calls anthropic.messages.create() without passing a max_tokens parameter, allowing unbounded token consumption and potential cost explosion or denial of service. Add a max_tokens parameter to the anthropic.messages.create() call in haiku_classifier.py: set it to a reasonable limit (e.g., 500 tokens) based on the expected classification output size. This caps the cost and latency of each classification call. Verify by calling any of the four trigger tools (feedback_classifier, schema_monitor, anomaly_reviewer, digest_generator) and confirming the Anthropic API request includes max_tokens.
digest_generator tool calls classify() which invokes Anthropic SDK without max_tokens cap
Evidence
| 1 | """ |
| 2 | datanexus/agents/digest_generator.py โ Haiku Trigger 4: weekly digest. |
| 3 | |
| 4 | Spec: DataNexus_MCP_Spec_v7_4.docx Section 13.6 / Phase 6 |
| 5 | |
| 6 | Batches ALL feedback records for a tool into ONE Haiku call (cost control) |
| 7 | and produces a DigestItem with data quality score, top issues, suggested |
| 8 | rules, and sprint recommendations. |
| 9 | |
| 10 | Rules (CLAUDE.md): |
| 11 | S13-1: This is Haiku Trigger T4. Delegate to haiku_classifier.classify() only. |
| 12 | S13-4: Never raises. Always returns structured DigestItem. |
| 13 | |
| 14 | Special case โ emp |
RemediationAI
The problem is that the digest_generator tool calls the classify() function which invokes the Anthropic SDK without a max_tokens cap, allowing unbounded token consumption and potential cost explosion or denial of service. Add a max_tokens parameter to the anthropic.messages.create() call in the classify() function: set it to a reasonable limit (e.g., 1000 tokens for digest generation) based on the expected output size. This caps the cost and latency of each digest generation call. Verify by calling the digest_generator tool and confirming the Anthropic API call includes max_tokens in the request.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 250 | { "name": "NPPES NPI Registry (CMS)", "url": "https://npiregistry.cms.hhs.gov/api/", "auth": "none" }, |
| 251 | { "name": "FINRA BrokerCheck", "url": "https://api.brokercheck.finra.org", "auth": "none" }, |
| 252 | { "name": "SAM.gov", "url": "https://api.sam.gov", "auth": "none" }, |
| 253 | { "name": "IANA RDAP", "url": "https://rdap.org", "auth": "none" }, |
| 254 | { "name": "crt.sh (Certificate Transparency)", "url": "https://crt.sh", "auth": "none" }, |
| 255 | { "name": "Cloudflare DoH", "url": "https://cloudflare-dn |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add a middleware or decorator to each tool handler that validates the caller's credentials before executing the tool. Alternatively, if the server must be unauthenticated, deploy it stdio-only behind a trusted host boundary (e.g., inside a private network or container) so that only trusted clients can reach it. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error; then call with valid credentials and confirming the tool executes.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 251 | { "name": "FINRA BrokerCheck", "url": "https://api.brokercheck.finra.org", "auth": "none" }, |
| 252 | { "name": "SAM.gov", "url": "https://api.sam.gov", "auth": "none" }, |
| 253 | { "name": "IANA RDAP", "url": "https://rdap.org", "auth": "none" }, |
| 254 | { "name": "crt.sh (Certificate Transparency)", "url": "https://crt.sh", "auth": "none" }, |
| 255 | { "name": "Cloudflare DoH", "url": "https://cloudflare-dns.com/dns-query", "auth": "none" }, |
| 256 | { "name": "EPO OPS (Open Patent Services)", "url": "https://ops.epo |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 245 | { "name": "IRS TEOS", "url": "https://apps.irs.gov/app/eos/", "auth": "none" }, |
| 246 | { "name": "Charity Commission England and Wales (CCEW)", "url": "https://api.charitycommission.gov.uk", "auth": "none" }, |
| 247 | { "name": "Google OSV.dev", "url": "https://api.osv.dev/v1", "auth": "none" }, |
| 248 | { "name": "NIST NVD", "url": "https://services.nvd.nist.gov/rest/json/cves/2.0", "auth": "none" }, |
| 249 | { "name": "deps.dev (Google)", "url": "https://api.deps.dev", "auth": "none" }, |
| 250 | { "name": "NPPES NPI |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 252 | { "name": "SAM.gov", "url": "https://api.sam.gov", "auth": "none" }, |
| 253 | { "name": "IANA RDAP", "url": "https://rdap.org", "auth": "none" }, |
| 254 | { "name": "crt.sh (Certificate Transparency)", "url": "https://crt.sh", "auth": "none" }, |
| 255 | { "name": "Cloudflare DoH", "url": "https://cloudflare-dns.com/dns-query", "auth": "none" }, |
| 256 | { "name": "EPO OPS (Open Patent Services)", "url": "https://ops.epo.org/3.2/rest-services", "auth": "oauth2" }, |
| 257 | { "name": "USASpending.gov", "url": "https://api |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 255 | { "name": "Cloudflare DoH", "url": "https://cloudflare-dns.com/dns-query", "auth": "none" }, |
| 256 | { "name": "EPO OPS (Open Patent Services)", "url": "https://ops.epo.org/3.2/rest-services", "auth": "oauth2" }, |
| 257 | { "name": "USASpending.gov", "url": "https://api.usaspending.gov", "auth": "none" }, |
| 258 | { "name": "Federal Register API", "url": "https://www.federalregister.gov/api/v1", "auth": "none" }, |
| 259 | { "name": "Regulations.gov", "url": "https://api.regulations.gov/v4", "auth": "none" } |
| 260 | ], |
| 261 | |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 243 | "data_sources": [ |
| 244 | { "name": "IRS EO BMF", "url": "https://www.irs.gov/charities-non-profits/exempt-organizations-business-master-file-extract-eo-bmf", "auth": "none" }, |
| 245 | { "name": "IRS TEOS", "url": "https://apps.irs.gov/app/eos/", "auth": "none" }, |
| 246 | { "name": "Charity Commission England and Wales (CCEW)", "url": "https://api.charitycommission.gov.uk", "auth": "none" }, |
| 247 | { "name": "Google OSV.dev", "url": "https://api.osv.dev/v1", "auth": "none" }, |
| 248 | { "name": "NIST NVD", "url": " |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 241 | } |
| 242 | ], |
| 243 | "data_sources": [ |
| 244 | { "name": "IRS EO BMF", "url": "https://www.irs.gov/charities-non-profits/exempt-organizations-business-master-file-extract-eo-bmf", "auth": "none" }, |
| 245 | { "name": "IRS TEOS", "url": "https://apps.irs.gov/app/eos/", "auth": "none" }, |
| 246 | { "name": "Charity Commission England and Wales (CCEW)", "url": "https://api.charitycommission.gov.uk", "auth": "none" }, |
| 247 | { "name": "Google OSV.dev", "url": "https://api.osv.dev/v1", "auth": "none" }, |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 246 | { "name": "Charity Commission England and Wales (CCEW)", "url": "https://api.charitycommission.gov.uk", "auth": "none" }, |
| 247 | { "name": "Google OSV.dev", "url": "https://api.osv.dev/v1", "auth": "none" }, |
| 248 | { "name": "NIST NVD", "url": "https://services.nvd.nist.gov/rest/json/cves/2.0", "auth": "none" }, |
| 249 | { "name": "deps.dev (Google)", "url": "https://api.deps.dev", "auth": "none" }, |
| 250 | { "name": "NPPES NPI Registry (CMS)", "url": "https://npiregistry.cms.hhs.gov/api/", "auth": "none" }, |
| 251 | |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 247 | { "name": "Google OSV.dev", "url": "https://api.osv.dev/v1", "auth": "none" }, |
| 248 | { "name": "NIST NVD", "url": "https://services.nvd.nist.gov/rest/json/cves/2.0", "auth": "none" }, |
| 249 | { "name": "deps.dev (Google)", "url": "https://api.deps.dev", "auth": "none" }, |
| 250 | { "name": "NPPES NPI Registry (CMS)", "url": "https://npiregistry.cms.hhs.gov/api/", "auth": "none" }, |
| 251 | { "name": "FINRA BrokerCheck", "url": "https://api.brokercheck.finra.org", "auth": "none" }, |
| 252 | { "name": "SAM.gov", "url" |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 256 | { "name": "EPO OPS (Open Patent Services)", "url": "https://ops.epo.org/3.2/rest-services", "auth": "oauth2" }, |
| 257 | { "name": "USASpending.gov", "url": "https://api.usaspending.gov", "auth": "none" }, |
| 258 | { "name": "Federal Register API", "url": "https://www.federalregister.gov/api/v1", "auth": "none" }, |
| 259 | { "name": "Regulations.gov", "url": "https://api.regulations.gov/v4", "auth": "none" } |
| 260 | ], |
| 261 | "tools_count": 30, |
| 262 | "cache_ttl_seconds": 86400, |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 248 | { "name": "NIST NVD", "url": "https://services.nvd.nist.gov/rest/json/cves/2.0", "auth": "none" }, |
| 249 | { "name": "deps.dev (Google)", "url": "https://api.deps.dev", "auth": "none" }, |
| 250 | { "name": "NPPES NPI Registry (CMS)", "url": "https://npiregistry.cms.hhs.gov/api/", "auth": "none" }, |
| 251 | { "name": "FINRA BrokerCheck", "url": "https://api.brokercheck.finra.org", "auth": "none" }, |
| 252 | { "name": "SAM.gov", "url": "https://api.sam.gov", "auth": "none" }, |
| 253 | { "name": "IANA RDAP", "url": "https |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 242 | ], |
| 243 | "data_sources": [ |
| 244 | { "name": "IRS EO BMF", "url": "https://www.irs.gov/charities-non-profits/exempt-organizations-business-master-file-extract-eo-bmf", "auth": "none" }, |
| 245 | { "name": "IRS TEOS", "url": "https://apps.irs.gov/app/eos/", "auth": "none" }, |
| 246 | { "name": "Charity Commission England and Wales (CCEW)", "url": "https://api.charitycommission.gov.uk", "auth": "none" }, |
| 247 | { "name": "Google OSV.dev", "url": "https://api.osv.dev/v1", "auth": "none" }, |
| 248 | { "name": "NIST NVD", "ur |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
Manifest explicitly declares authentication is OFF (`"auth": "none"` / `false` / `null` / `"authentication": null`). The author considered auth and declined it โ every tool call reaches the server without credentials. Set a real mechanism (bearer, oauth, mtls, apiKey) or run the server stdio-only behind a trusted host boundary.
Evidence
| 244 | { "name": "IRS EO BMF", "url": "https://www.irs.gov/charities-non-profits/exempt-organizations-business-master-file-extract-eo-bmf", "auth": "none" }, |
| 245 | { "name": "IRS TEOS", "url": "https://apps.irs.gov/app/eos/", "auth": "none" }, |
| 246 | { "name": "Charity Commission England and Wales (CCEW)", "url": "https://api.charitycommission.gov.uk", "auth": "none" }, |
| 247 | { "name": "Google OSV.dev", "url": "https://api.osv.dev/v1", "auth": "none" }, |
| 248 | { "name": "NIST NVD", "url": "https://services.nvd.n |
RemediationAI
The problem is that the manifest explicitly declares authentication is OFF (`"auth": "none"`), meaning every tool call reaches the server without credentials, allowing any caller to invoke any tool. Implement a real authentication mechanism: add a bearer token, OAuth 2.0, mTLS, or API key scheme to the manifest's authentication field, and add middleware to each tool handler that validates the caller's credentials before executing the tool. Alternatively, deploy the server stdio-only behind a trusted host boundary. This prevents unauthorized tool invocation. Verify by calling a tool without credentials and confirming the server returns a 401 Unauthorized error.
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 | |
RemediationAI
The problem is that datanexus/tools/t11.py holds a mutable shared container (e.g., a process-global list or cache) that is mutated from inside a tool handler with no caller-identity marker (user_id, session_id, caller_id, request_id, org_id, tenant_id, actor_id, subject, or principal), allowing a later caller to read what an earlier caller wrote and causing cross-request data leakage. Add a caller-identity parameter to the tool input schema, thread it through the tool handler, and partition all mutable state by caller ID (e.g., use a dict keyed by caller_id instead of a single shared list). This ensures each caller's data is isolated. Verify by calling the tool twice with different caller IDs, confirming that the second caller cannot see data written by the first caller.
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 |
RemediationAI
The problem is that feedback/collector.py uses Python's `global` keyword to mutate module-level state (_redis_client) from inside a function, which in a multi-tenant MCP server creates a cross-request data path where one caller's state affects another caller's requests. Replace the global variable with a per-request context object: pass the Redis client as a function parameter or retrieve it from a request-scoped context (e.g., using contextvars.ContextVar or a thread-local storage keyed by request ID). This ensures each request has its own isolated state. Verify by calling the tool twice concurrently with different caller IDs and confirming that each request uses its own Redis client instance.
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: |
RemediationAI
The problem is that datanexus/tools/frontend_sprint8.py uses Python's `global` keyword to mutate module-level state (_FRONTEND_CORPUS) from inside a function, which in a multi-tenant MCP server creates a cross-request data path where one caller's state affects another caller's requests. Replace the global variable with a per-request context object or a class-based approach: pass the corpus as a function parameter or retrieve it from a request-scoped context (e.g., using contextvars.ContextVar). This ensures each request has its own isolated state. Verify by calling the tool twice concurrently with different caller IDs and confirming that each request uses its own corpus instance.
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: |
RemediationAI
The problem is that payment/entitlement.py uses Python's `global` keyword to mutate module-level state (_redis_client) from inside a function, which in a multi-tenant MCP server creates a cross-request data path where one caller's state affects another caller's requests. Replace the global variable with a per-request context object: pass the Redis client as a function parameter or retrieve it from a request-scoped context (e.g., using contextvars.ContextVar). This ensures each request has its own isolated state. Verify by calling the tool twice concurrently with different caller IDs and confirming that each request uses its own Redis client instance.
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 |
RemediationAI
The problem is that datanexus/tools/t10.py holds a mutable shared container (e.g., a process-global list or cache) that is mutated from inside a tool handler with no caller-identity marker, allowing a later caller to read what an earlier caller wrote and causing cross-request data leakage. Add a caller-identity parameter to the tool input schema, thread it through the tool handler, and partition all mutable state by caller ID (e.g., use a dict keyed by caller_id instead of a single shared list). This ensures each caller's data is isolated. Verify by calling the tool twice with different caller IDs, confirming that the second caller cannot see data written by the first caller.
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 |
RemediationAI
The problem is that payment/entitlement.py uses Python's `global` keyword to mutate module-level state (_redis_client) from inside a function, which in a multi-tenant MCP server creates a cross-request data path where one caller's state affects another caller's requests. Replace the global variable with a per-request context object: pass the Redis client as a function parameter or retrieve it from a request-scoped context (e.g., using contextvars.ContextVar). This ensures each request has its own isolated state. Verify by calling the tool twice concurrently with different caller IDs and confirming that each request uses its own Redis client instance.
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 |
RemediationAI
The problem is that datanexus/tools/nonprofit_sprint7.py holds a mutable shared container (e.g., a process-global list or cache) that is mutated from inside a tool handler with no caller-identity marker, allowing a later caller to read what an earlier caller wrote and causing cross-request data leakage. Add a caller-identity parameter to the tool input schema, thread it through the tool handler, and partition all mutable state by caller ID (e.g., use a dict keyed by caller_id instead of a single shared list). This ensures each caller's data is isolated. Verify by calling the tool twice with different caller IDs, confirming that the second caller cannot see data written by the first caller.
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 |
RemediationAI
The problem is that datanexus/tools/security_sprint6.py holds a mutable shared container (e.g., a process-global list or cache) that is mutated from inside a tool handler with no caller-identity marker, allowing a later caller to read what an earlier caller wrote and causing cross-request data leakage. Add a caller-identity parameter to the tool input schema, thread it through the tool handler, and partition all mutable state by caller ID (e.g., use a dict keyed by caller_id instead of a single shared list). This ensures each caller's data is isolated. Verify by calling the tool twice with different caller IDs, confirming that the second caller cannot see data written by the first caller.
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() |
RemediationAI
The problem is that feedback/collector.py uses Python's `global` keyword to mutate module-level state (_redis_client) from inside a function, which in a multi-tenant MCP server creates a cross-request data path where one caller's state affects another caller's requests. Replace the global variable with a per-request context object: pass the Redis client as a function parameter or retrieve it from a request-scoped context (e.g., using contextvars.ContextVar). This ensures each request has its own isolated state. Verify by calling the tool twice concurrently with different caller IDs and confirming that each request uses its own Redis client instance.
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 |
RemediationAI
The problem is that payment/entitlement.py uses Python's `global` keyword to mutate module-level state (_test_caller_id) from inside a function, which in a multi-tenant MCP server creates a cross-request data path where one caller's state affects another caller's requests. Replace the global variable with a per-request context object: pass the caller_id as a function parameter or retrieve it from a request-scoped context (e.g., using contextvars.ContextVar). This ensures each request has its own isolated state. Verify by calling the tool twice concurrently with different caller IDs and confirming that each request uses its own caller_id instance.
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=
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 | # โโ Structured error response shape โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
| 84 | # Return this dict on ANY error. Never raise. Never return str(e). |
| 85 | # error_code must come from ErrorCode enum below. |
| 86 | # |
| 87 | # { |
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.
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
| 180 | "error": str(exc), |
| 181 | "haiku_available": False, |
| 182 | })) |
| 183 | return {"error": str(exc), "haiku_available": False} |
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.