← All MCP servers
MCP server

Midas

mcp:vornicx/midas

Local-first, source-traceable agent memory — no LLM at ingest, fully offline

Verdict
Suspicious

Risky behavior the analysis could not rule out.

Analyzed Oct 26, 2026, 13:29 UTC at ee9953c
3
findings
high
highest severity
2
threat classes
174
files scanned
1,813/1,813
checks satisfied
0
package advisories

Why this verdict

mcp-server . Suspicious
  • suspicious: finding mcp-server:.:mcpb/manifest.json:2:SUPPLY
  • suspicious: finding mcp-server:.:mcpb/server/main.py:3:SUPPLY
  • suspicious: finding mcp-server:.:midas/inspector.py:969:DESTRUCT
mcp-server packages/midas-ts No issues found
  • benign: 328 checks satisfied, no findings for mcp-server:packages/midas-ts

Security findings 3

Supply chain

highContextual risklow confidence

The MCPB manifest launches the server with `uvx midas-memory-mcp` with no version pin, so whatever the latest PyPI release is gets fetched and executed at runtime (server.json pins a different version, 0.0.1).

server.mcp_config.command: uvx
conceal:remotevector:metadata
Counter-argument considered

The manifest launches the package with uvx and no version pin, so the carve-out for pinned registry installs does not apply. The mechanism is openly described in main.py, but the unpinned runtime fetch is still real, so the finding is not refuted. Severity is low.

Destructive action

highContextual risklow confidence

The local inspector HTTP server accepts unauthenticated POSTs with no Origin, CSRF or content-type check, so a web page in the user's browser could trigger deletion of memory records via /api/forget.

return self._json(api_forget(mem, body.get("id", "")))
vector:code
Counter-argument considered

The POST handler parses JSON regardless of content-type and checks no Origin or token, so a cross-origin page could trigger a forget on the localhost server. Nothing cited neutralizes this, so the finding stands.

Supply chain

mediumContextual risklow confidence

The MCPB bundle's entry point is a stub that runs `uvx midas-memory-mcp`. That resolves an unpinned PyPI package (`midas-memory[mcp,local]>=0.0.1`) at runtime, so the code actually executed is not shipped in the bundle.

The packaged manifest runs Midas via `uvx midas-memory-mcp`
vector:metadata
Counter-argument considered

The stub docstring confirms the bundle resolves its code from PyPI at runtime rather than shipping it. The package is unpinned, so the pinned-install carve-out does not apply. The behavior is disclosed and low severity, but it is not refuted.

Open questions 5

What the analysis could not settle from the code alone.

The server-level MCP `instructions` text (injected recall->work->capture loop and policy) is truncated in the shown units; does it steer the agent beyond the declared memory task?
Agent hijacking
Does the MCP `distill` tool let the calling agent choose the distiller host/base_url or api_key_env, so that memory text and an env secret could be sent to an arbitrary remote endpoint, or is the endpoint fixed by the operator's local config?
Data exfiltration
What does midas.hooks.install_claude_hook write into ~/.claude/settings.json, and what does capture_session store and where? The code is not shown in this batch.
Persistence
[file request midas/mcp_server.py] Where is AGENT_MEMORY_INSTRUCTIONS defined or imported from, and what does that text tell the agent (autonomous capture, secrecy from the user, conditioning of other tools)?
Agent hijacking
[file request midas/mcp_server.py] What does the server `instructions` / auto-memory policy text tell the agent (autonomous capture, secrecy, conditioning other tools)?
Agent hijacking
Refuted findings 14

Candidates the review stage examined and rejected, kept for transparency.

Persistence

criticalRefutedmedium confidence

The `midas init` CLI writes or merges a `midas` MCP server entry into the config files of several agent clients (with a backup) and installs a Claude Code SessionEnd hook that captures session turns into memory; this is the declared setup function but it is a control-plane write.

non-destructively (merge + backup)
Why it was refuted

Writing the MCP server entry into client configs, with a backup, is the declared purpose of `midas init`. The Claude hook is installed only behind an explicit --claude-hook flag. This is the declared core capability, not concealed persistence.

Persistence

criticalRefutedmedium confidence

build_memory defaults to a persistent shared SQLite store at ~/.midas/memory.sqlite3 (creating the directory) even though the module docstring says the default is in-memory, so memories from all MCP clients silently persist on disk and are shared across clients.

db = os.path.expanduser("~/.midas/memory.sqlite3")
Why it was refuted

Persistent shared storage is the core capability of a memory server and is disclosed in the code comment, with an override via MIDAS_MCP_DB. The stale docstring does not make the behavior concealed or harmful.

Data exfiltration

highRefutedmedium confidence

The eval harness (eval/llm.py) hardcodes third-party LLM endpoints (opencode.ai, api.groq.com, openrouter.ai, api.mistral.ai) and posts benchmark questions and retrieved memory context to them, which is undeclared and contradicts the repo's 'no network / no telemetry / data never leaves your computer' claims; it is an opt-in judge tool using user-supplied API keys, not a hidden channel.

"https://opencode.ai/zen/go/v1"
Why it was refuted

The endpoints are in the optional LLM-judge eval harness. It is dev tooling that is not shipped in the installed wheel, runs only with --judge and uses the user's own provider keys. It is not part of the MCP server's memory path, and no source-to-sink chain from stored memory is shown.

Data exfiltration

highRefutedlow confidence

ChatLLM creates an httpx client and POSTs prompts (including assembled memory context) to a dynamic base_url chosen from env-provided keys, contradicting the privacy claims that no memory data is transmitted; this is confined to the optional eval/judge path.

self._client = httpx.Client(timeout=120.0)
Why it was refuted

The httpx client is created only inside the optional --judge eval path. It sends eval prompts to the provider the user configured, not the user's stored memory, and no chain from the server's memory store is shown.

Data exfiltration

highRefutedlow confidence

Eval-only script eval/summarization_ab.py POSTs conversation turns over HTTP to a configurable Ollama host (default localhost), while the repo claims no data ever leaves the machine; the network call is undeclared in the privacy claims, though it is local by default and part of an offline benchmark harness.

with urllib.request.urlopen(req, timeout=self.timeout) as resp:
Why it was refuted

This is an eval-only A/B script that defaults to a local Ollama host at localhost, so data stays on the machine. It is not part of the shipped server and sends nothing off-host by default.

Data exfiltration

highRefutedmedium confidence

The opt-in HTTPDistiller (and OllamaDistiller) POSTs conversation text to a configurable host and can send an environment-variable API key as a Bearer token to any base_url, which contradicts the 'data never leaves your computer / no memory data transmitted over the network' claims, though it is documented as an explicit opt-in tier.

"Authorization": f"Bearer {self.api_key}"
Why it was refuted

The served mcp_server.py never imports or constructs HTTPDistiller or OllamaDistiller. Its `distill` entry is a prompt that returns instruction text for the agent's own model and makes no network call. base_url, host and api_key come only from operator-side constructor arguments, so no agent-controlled chain reaches the Authorization header. The cloud tradeoff is stated in the class docstring, and the key is the wrapped service's own API key.

Persistence

highRefutedmedium confidence

Opt-in `midas init --claude-hook` writes a SessionEnd command hook into the user's ~/.claude/settings.json (with backup) that auto-captures user transcript turns into memory; this agent control-plane write is disclosed, but not covered by the 'local memory' safety claims.

matchers.append({"hooks": [hook_settings_block()]})
Why it was refuted

The hook is opt-in via an explicit CLI command and writes a fixed command with merge, backup and a matching uninstall. It is documented in the module docstring, so it is a validated hook shim with safe cleanup. No agent instruction triggers it.

Persistence

highRefutedlow confidence

The optional `midas init --claude-hook` writes a SessionEnd hook into the agent control-plane file ~/.claude/settings.json, which reads session transcripts and stores user turns into memory. This is not listed among the declared MCP tools, but it is documented, opt-in, and local-only.

`midas init --claude-hook` registers a SessionEnd hook in `~/.claude/settings.json`
Why it was refuted

This is the same opt-in, documented SessionEnd hook with backup and uninstall. It is a validated hook shim with safe cleanup, and the docstring declares it, so it is not concealed.

Persistence

highRefutedlow confidence

Opt-in `midas init --claude-hook` merges a SessionEnd hook into ~/.claude/settings.json that reads each session transcript and offers user turns to local memory; this is a disclosed agent control-plane write with backup, and no network sink is visible.

`midas init --claude-hook` registers a SessionEnd hook in `~/.claude/settings.json`
Why it was refuted

This is the same opt-in, documented hook shim with merge, backup and uninstall, and no network sink is shown. The user runs the CLI explicitly, and no instruction directs the agent to do so.

Destructive action

highRefutedlow confidence

The forget_all MCP tool wipes the entire memory store with a single call and no confirmation or dry-run, which is a destructive capability, though it is declared, annotated destructiveHint=True, and fits a memory server's purpose.

_mem.store.clear()
Why it was refuted

forget_all is a declared memory-management tool, annotated destructiveHint=True, and clearing memory is core to the server. This is the declared-core-capability carve-out.

Destructive action

highRefutedlow confidence

The forget_all MCP tool wipes the entire memory store in one call with no confirmation or dry-run, so an agent invocation can irreversibly delete all stored memories (consistent with the declared memory-management purpose).

mem.store.clear();
Why it was refuted

forget_all is a declared tool whose description states it clears all memories, which is the core capability of a memory server. Nothing is concealed or beyond its stated purpose.

Credential access

mediumRefutedlow confidence

load_dotenv in the eval harness reads the repo-root .env and loads all of its entries into os.environ (not just provider keys); undeclared but used only to supply API keys to the eval LLM client, with no observed exfiltration beyond the chosen provider's Authorization header.

os.environ.setdefault(key.strip(), val.strip())
Why it was refuted

The .env loader only populates os.environ with setdefault so the eval client can find the user's own provider keys. No sink for other .env entries is shown, so this is the carve-out for env API keys for the wrapped service in dev-only tooling.

Data exfiltration

mediumRefutedlow confidence

The optional eval LLM client reads provider API keys from the environment or .env and POSTs benchmark context and questions with a Bearer token to third-party hosted LLM endpoints (opencode.ai, groq, openrouter, mistral), which conflicts with the 'no telemetry' claim; the data is eval-dataset content for the declared judge feature.

f"{self.base_url}/chat/completions", headers=headers, json=payload
Why it was refuted

The Bearer-token POST is the standard OpenAI-compatible judge call. It uses the user-supplied key for the chosen provider, is opt-in through --judge, and lives in dev-only eval tooling.

Agent hijacking

mediumRefutedlow confidence

The resume tool description tells the agent to use the returned context silently, which could suppress disclosure of injected memory content to the user.

use it silently, then work
Why it was refuted

'Use it silently' tells the agent not to narrate the prompt-ready context block. It does not instruct concealment of anything from the user or any action, and it is not a hijack.