Mostly safe โ a couple of notes worth reading.
Scanned 6/22/2026, 4:21:01 PMยทCached resultยทFast Scanยท48 rulesยทHow we decide โ
AIVSS Score
Low
Severity Breakdown
0
critical
0
high
24
medium
2
low
MCP Server Information
Findings
This package receives a B grade with a safety score of 65/100, driven primarily by 24 medium-severity issues concentrated in verbose error handling (16 findings) and server configuration (8 findings). While no critical or high-severity vulnerabilities exist, the verbose error outputs could leak sensitive information and the configuration gaps may impact operational security. You should review and remediate the medium-severity findings before deployment, particularly around error message sanitization and hardening server settings.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 26 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.
26 of 26 findings
26 findings
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
| 167 | return json.dumps({"error": str(e), "error_code": "invalid_patch", "path": path}) |
| 168 | except Exception as e: |
| 169 | logger.error("vault_apply_unified_diff error for %s: %s", path, e) |
| 170 | return json.dumps({"error": str(e), "error_code": "internal_error", "path": path}) |
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
| 162 | except FileNotFoundError: |
| 163 | return json.dumps({"error": f"File not found: {path}", "error_code": "file_not_found", "path": path}) |
| 164 | except PatchError as e: |
| 165 | return json.dumps({"error": str(e), "error_code": e.code, "path": path}) |
| 166 | except ValueError as e: |
| 167 | return json.dumps({"error": str(e), "error_code": "invalid_patch", "path": path}) |
| 168 | except Exception as e: |
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
| 31 | return json.dumps({"error": f"Directory not found: {path}"}) |
| 32 | except Exception as e: |
| 33 | logger.error(f"vault_list error: {e}") |
| 34 | return json.dumps({"error": str(e)}) |
| 35 | |
| 36 | |
| 37 | def vault_move(source: str, destination: str, create_dirs: bool = True) -> str: |
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
| 58 | deleted = delete_path(path) |
| 59 | return json.dumps({"path": path, "deleted": deleted}) |
| 60 | except ValueError as e: |
| 61 | return json.dumps({"error": str(e), "path": path}) |
| 62 | except Exception as e: |
| 63 | logger.error(f"vault_delete error: {e}") |
| 64 | return json.dumps({"error": str(e), "path": path}) |
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
| 40 | moved = move_path(source, destination, create_dirs=create_dirs) |
| 41 | return json.dumps({"source": source, "destination": destination, "moved": moved}) |
| 42 | except ValueError as e: |
| 43 | return json.dumps({"error": str(e), "source": source, "destination": destination}) |
| 44 | except Exception as e: |
| 45 | logger.error(f"vault_move error: {e}") |
| 46 | return json.dumps({"error": str(e), "source": source, "destination": destination}) |
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
| 61 | return json.dumps({"error": str(e), "path": path}) |
| 62 | except Exception as e: |
| 63 | logger.error(f"vault_delete error: {e}") |
| 64 | return json.dumps({"error": str(e), "path": path}) |
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
| 43 | return json.dumps({"error": str(e), "source": source, "destination": destination}) |
| 44 | except Exception as e: |
| 45 | logger.error(f"vault_move error: {e}") |
| 46 | return json.dumps({"error": str(e), "source": source, "destination": destination}) |
| 47 | |
| 48 | |
| 49 | def vault_delete(path: str, confirm: bool = False) -> str: |
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
| 164 | except PatchError as e: |
| 165 | return json.dumps({"error": str(e), "error_code": e.code, "path": path}) |
| 166 | except ValueError as e: |
| 167 | return json.dumps({"error": str(e), "error_code": "invalid_patch", "path": path}) |
| 168 | except Exception as e: |
| 169 | logger.error("vault_apply_unified_diff error for %s: %s", path, e) |
| 170 | return json.dumps({"error": str(e), "error_code": "internal_error", "path": path}) |
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
| 295 | "truncated": truncated, |
| 296 | }, default=str) |
| 297 | except ValueError as e: |
| 298 | return json.dumps({"error": str(e)}) |
| 299 | except Exception as e: |
| 300 | logger.exception("vault_search error: %s", e) |
| 301 | return json.dumps({"error": str(e)}) |
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
| 27 | return json.dumps({"path": path, "created": is_new, "size": size}) |
| 28 | except ValueError as e: |
| 29 | return json.dumps({"error": str(e), "path": path}) |
| 30 | except Exception as e: |
| 31 | logger.error(f"vault_create_overwrite_file error for {path}: {e}") |
| 32 | return json.dumps({"error": str(e), "path": path}) |
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
| 30 | return json.dumps({"error": str(e), "path": path}) |
| 31 | except Exception as e: |
| 32 | logger.error(f"vault_create_overwrite_file error for {path}: {e}") |
| 33 | return json.dumps({"error": str(e), "path": path}) |
| 34 | |
| 35 | |
| 36 | def vault_batch_frontmatter_update(updates: list[dict]) -> str: |
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
| 83 | "has_trailing_newline": content.endswith("\n"), |
| 84 | }, default=str) |
| 85 | except ValueError as e: |
| 86 | return json.dumps({"error": str(e), "path": path}) |
| 87 | except FileNotFoundError: |
| 88 | return json.dumps({"error": f"File not found: {path}", "path": path}) |
| 89 | except Exception as e: |
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
| 88 | return json.dumps({"error": f"File not found: {path}", "path": path}) |
| 89 | except Exception as e: |
| 90 | logger.error(f"vault_read error for {path}: {e}") |
| 91 | return json.dumps({"error": str(e), "path": path}) |
| 92 | |
| 93 | |
| 94 | def vault_batch_read(paths: list[str], include_content: bool = True) -> str: |
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
| 339 | }, default=str) |
| 340 | except Exception as e: |
| 341 | logger.error(f"vault_search_frontmatter error: {e}") |
| 342 | return json.dumps({"error": str(e)}) |
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
| 26 | ) |
| 27 | return json.dumps({"items": items, "total": len(items)}) |
| 28 | except ValueError as e: |
| 29 | return json.dumps({"error": str(e)}) |
| 30 | except FileNotFoundError: |
| 31 | return json.dumps({"error": f"Directory not found: {path}"}) |
| 32 | except Exception as e: |
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
| 298 | return json.dumps({"error": str(e)}) |
| 299 | except Exception as e: |
| 300 | logger.exception("vault_search error: %s", e) |
| 301 | return json.dumps({"error": str(e)}) |
| 302 | |
| 303 | |
| 304 | def vault_search_frontmatter( |
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.
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 500 | }, |
| 501 | ) |
| 502 | def vault_list( |
| 503 | path: str = "", |
| 504 | depth: int = 1, |
| 505 | include_files: bool = True, |
| 506 | include_dirs: bool = True, |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 159 | """Delete a file from the vault.""" |
| 160 | |
| 161 | model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") |
| 162 | |
| 163 | path: str = Field( |
| 164 | ..., |
| 165 | description="Relative path of the file to delete", |
| 166 | min_length=1, |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 176 | """Full-text search across vault files.""" |
| 177 | |
| 178 | model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") |
| 179 | |
| 180 | query: str = Field( |
| 181 | ..., |
| 182 | description="Search string to find in file contents", |
| 183 | min_length=1, |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 76 | """Apply a unified diff patch to an existing text file.""" |
| 77 | |
| 78 | model_config = ConfigDict(str_strip_whitespace=False, extra="forbid") |
| 79 | |
| 80 | path: str = Field( |
| 81 | ..., |
| 82 | description="Relative path from vault root", |
| 83 | min_length=1, |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 9 | def vault_list( |
| 10 | path: str = "", |
| 11 | depth: int = 1, |
| 12 | include_files: bool = True, |
| 13 | include_dirs: bool = True, |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 105 | """List files and directories under a vault path.""" |
| 106 | |
| 107 | model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") |
| 108 | |
| 109 | path: str = Field( |
| 110 | default="", |
| 111 | description="Relative directory path from vault root; empty string for root", |
| 112 | max_length=500, |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 18 | """Read a single file from the vault.""" |
| 19 | |
| 20 | model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") |
| 21 | |
| 22 | path: str = Field( |
| 23 | ..., |
| 24 | description="Relative path from vault root (e.g. 'projects/acme/notes.md')", |
| 25 | min_length=1, |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 54 | """Create a new file or overwrite an existing file in the vault.""" |
| 55 | |
| 56 | model_config = ConfigDict(str_strip_whitespace=False, extra="forbid") |
| 57 | |
| 58 | path: str = Field( |
| 59 | ..., |
| 60 | description="Relative path from vault root", |
| 61 | min_length=1, |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
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
| 47 | try: |
| 48 | post = frontmatter.loads(content) |
| 49 | if post.metadata: |
| 50 | return post.metadata |
| 51 | except Exception: |
| 52 | pass |
| 53 | return None |
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
| 92 | except BaseException: |
| 93 | # Clean up the temp file on any failure |
| 94 | try: |
| 95 | os.unlink(tmp_path) |
| 96 | except OSError: |
| 97 | pass |
| 98 | raise |
| 99 | |
| 100 | return is_new, len(encoded) |
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.