Skip to content

MCP Protocol Reference

The ZettelForge MCP server implements the Model Context Protocol specification (protocol version 2024-11-05) over stdio transport using JSON-RPC 2.0.

Protocol overview

Property Value
Transport stdio (stdin for requests, stdout for responses)
Protocol JSON-RPC 2.0
MCP version 2024-11-05
Server name zettelforge
Tools available 7
Version reported 2.7.0
Lazy initialization MemoryManager is instantiated on first tool call, not on import

Lifecycle

The MCP lifecycle has three phases:

client                          server
  |                               |
  |--- initialize --------------->|  Phase 1: Initialization
  |<-- initialize result ---------|
  |--- notifications/initialized->|  (no response)
  |                               |
  |--- tools/list --------------->|  Phase 2: Tool discovery
  |<-- tools list ----------------|
  |                               |
  |--- tools/call --------------->|  Phase 3: Tool execution
  |<-- tool result ---------------|
  |                               |
  |--- tools/call --------------->|
  |<-- tool result ---------------|

Lazy singleton contract

Importing zettelforge.mcp (or zettelforge.mcp.server) does not instantiate MemoryManager. The initialize and tools/list methods work without touching the backend. MemoryManager is created on the first tools/call that reaches handle_tool_call(). Tool introspection is therefore side-effect-free.

from zettelforge.mcp import TOOLS, run_stdio

# TOOLS is a static list — no backend started
assert len(TOOLS) == 7

# run_stdio reads stdin, processes requests, writes stdout
run_stdio()

TLP access control

Every tool that reads from or writes to memory enforces TLP (Traffic Light Protocol) access control. The actor parameter identifies the caller:

  • No actor (default): unauthenticated calls are capped at TLP:GREEN. Notes marked TLP:AMBER, TLP:AMBER+STRICT, or TLP:RED are filtered from results.
  • Authenticated actor: a caller matching the configured single-user identity receives results up to TLP:RED.
  • TLP override: callers may request results above their sharing ceiling by providing a non-empty tlp_override_reason. The override is audited to a JSONL log.

The actor value is resolved from these argument keys in order: actor, caller, identity, user.

Default TLP for new notes is TLP:CLEAR.

Tool schemas

zettelforge_remember

Store threat intelligence in memory. Extracts entities (actors, CVEs, tools, campaigns) and populates the knowledge graph. With evolve=true (default), uses an LLM to compare against existing notes and decides whether to add, update, or supersede.

Input schema:

Property Type Required Default Description
content string yes Threat intelligence text to store
domain string no "cti" Domain: cti, incident, general
tlp string no "TLP:CLEAR" TLP marking for the note
actor string no Caller identity for audit attribution
source string no "mcp" Source reference string
evolve boolean no true Enable memory evolution (LLM-based dedup/merge)

TLP enum values: TLP:CLEAR, TLP:WHITE, TLP:GREEN, TLP:AMBER, TLP:AMBER+STRICT, TLP:RED (and short aliases without the TLP: prefix).

Request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "zettelforge_remember",
    "arguments": {
      "content": "APT28 used CVE-2024-3094 against NATO networks.",
      "domain": "cti",
      "source": "report-2026-001",
      "tlp": "TLP:AMBER",
      "evolve": true
    }
  }
}

Response fields:

Field Type Description
note_id string or null ID of the created/updated note, or null on error
status string "created", "updated", "corrected", "noop"
entities string[] Up to 10 extracted entity values
tlp string TLP marking applied to the stored note

zettelforge_recall

Search memory using blended vector + graph retrieval. Returns ranked results with entities, confidence scores, tier metadata, and TLP labels.

Input schema:

Property Type Required Default Description
query string yes Natural language search query
k integer no 10 Maximum number of results
domain string no Optional domain filter
actor string no Caller identity; unauthenticated calls are capped at TLP:GREEN
tlp_override_reason string no Audited reason to return content above the caller's sharing ceiling

Request:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "zettelforge_recall",
    "arguments": {
      "query": "What tools does APT28 use?",
      "k": 10,
      "domain": "cti"
    }
  }
}

Response fields:

Field Type Description
results object[] Ranked search results
results[].id string Note ID
results[].content string First 500 characters of note content, wrapped in BEGIN_UNTRUSTED_CONTENT … END_UNTRUSTED_CONTENT markers
results[].content_is_untrusted boolean Always true; content has passed prompt-injection screening but must be treated as untrusted
results[].context string Clean semantic context string (content without the UNTRUSTED wrapper, safe to display)
results[].entities string[] Up to 10 extracted entities
results[].tier string Epistemic tier: "A" (authoritative), "B" (operational, default), "C" (support)
results[].tlp string TLP marking of the note
results[].confidence number Confidence score (0.0 to 1.0)
count integer Number of results returned
latency_ms integer Query latency in milliseconds
tlp_share_max string Effective TLP ceiling applied to this response
tlp_override boolean Whether a TLP override was applied

zettelforge_synthesize

Generate a synthesized answer from ZettelForge memories using RAG (Retrieval-Augmented Generation). Requires a configured LLM provider; without one, returns a well-formed response with answer: "No specific answer found" and confidence: 0.0.

Input schema:

Property Type Required Default Description
query string yes Question to answer from memory
format string no "direct_answer" Output format (see below)
actor string no Caller identity; unauthenticated calls are capped at TLP:GREEN
tlp_override_reason string no Audited reason to use sources above the caller's sharing ceiling

Format values:

Value Description
direct_answer Concise answer with source attribution. Works in OSS with a configured LLM.
synthesized_brief Structured intelligence brief. Available via ThreatRecall.ai SaaS; falls back to direct_answer in OSS.
timeline_analysis Chronological event sequence. Available via ThreatRecall.ai SaaS; falls back to direct_answer in OSS.
relationship_map Entity relationship summary. Available via ThreatRecall.ai SaaS; falls back to direct_answer in OSS.

Request:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "zettelforge_synthesize",
    "arguments": {
      "query": "What is APT28's targeting pattern in 2026?",
      "format": "direct_answer"
    }
  }
}

Response fields:

Field Type Description
synthesis object The generated answer
synthesis.answer string The synthesized answer text; "No specific answer found" when no LLM is configured
synthesis.confidence number Answer confidence (0.0 to 1.0); 0.0 when no LLM is configured
synthesis.sources string[] Note IDs cited by the LLM in its answer; empty when no LLM is configured
sources_count integer Number of memory notes retrieved as candidates (non-zero even without LLM)
sources object[] Retrieved candidate notes (see fields below)
sources[].note_id string Note ID
sources[].relevance_score number Retrieval relevance score (0.0 to 1.0)
sources[].quote string First characters of note content, wrapped in BEGIN_UNTRUSTED_CONTENT … END_UNTRUSTED_CONTENT
sources[].tier string Epistemic tier: "A" (authoritative), "B" (operational), "C" (support)
sources[].tlp string TLP marking of the source note
tlp_max string Highest TLP level seen among sources
tlp_share_max string Effective TLP ceiling applied
tlp_override boolean Whether a TLP override was applied

zettelforge_entity

Fast entity lookup by type. Uses an O(1) index for direct entity-to-note mapping.

Input schema:

Property Type Required Default Description
type string yes Entity type (see common types below)
value string yes Entity value (e.g. "apt28", "CVE-2024-3094")
k integer no 5 Maximum results
actor string no Caller identity; unauthenticated calls are capped at TLP:GREEN
tlp_override_reason string no Audited reason to return content above the caller's sharing ceiling

Common entity types:

Type Indexing notes
intrusion_set Use for APT groups and named threat actors (e.g. "apt28", "lazarus"). This is the primary type for nation-state actors.
cve CVE identifiers (e.g. "CVE-2024-3094")
tool Malware and attack tooling names
campaign Named campaigns
attack_pattern MITRE ATT&CK technique identifiers and descriptions
actor Generic actor references not matched to a named intrusion set
person Individual names
domain Domain names (175+ unique in a typical deployment)
url URLs (434+ unique in a typical deployment)
ipv4 IPv4 addresses
md5, sha1, sha256 Indicator hashes
email Email addresses
location Geographic locations
organization Organization names

The entity indexer (entity_indexer.py) assigns APT-numbered groups (e.g. APT28, APT29) to intrusion_set, not actor. Pass the normalized lowercase form as value (e.g. "apt28", not "APT28").

Request:

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "zettelforge_entity",
    "arguments": {
      "type": "cve",
      "value": "CVE-2024-3094",
      "k": 5
    }
  }
}

Response fields:

Field Type Description
results object[] Notes referencing this entity
results[].id string Note ID
results[].content string First 300 characters of note content, wrapped in BEGIN_UNTRUSTED_CONTENT … END_UNTRUSTED_CONTENT
results[].content_is_untrusted boolean Always true; treat as untrusted
results[].tier string Epistemic tier: "A" (authoritative), "B" (operational), "C" (support)
results[].tlp string TLP marking of the note
count integer Number of results
tlp_share_max string Effective TLP ceiling applied
tlp_override boolean Whether a TLP override was applied

zettelforge_graph

Traverse the STIX 2.1 knowledge graph starting from a given entity. Shows relationships such as uses, targets, attributed-to. Returns up to 20 paths.

Input schema:

Property Type Required Default Description
type string yes Starting entity type
value string yes Starting entity value
max_depth integer no 2 Maximum traversal depth

Request:

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "zettelforge_graph",
    "arguments": {
      "type": "actor",
      "value": "apt28",
      "max_depth": 2
    }
  }
}

Response fields:

Field Type Description
paths object[][] Up to 20 graph traversal paths
paths[][].from string Source entity value
paths[][].rel string Relationship type
paths[][].to string Target entity value
count integer Number of paths found

zettelforge_stats

Return memory system statistics: version, total note count, retrieval count, and entity index breakdown.

Input schema: No parameters.

Request:

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "zettelforge_stats",
    "arguments": {}
  }
}

Response fields:

Field Type Description
version string ZettelForge version string (e.g. "2.7.0")
total_notes integer Total number of stored notes
retrievals integer Cumulative retrieval count since last restart
entity_index object Entity type counts; each key maps to {unique_entities, total_mappings}

The entity_index keys reflect what has been ingested. A populated deployment includes: cve, intrusion_set, actor, tool, campaign, attack_pattern, ipv4, domain, url, md5, sha1, sha256, email, person, location, organization, event, activity, temporal. Types with no data still appear with zero counts.


zettelforge_sync

Trigger an OpenCTI sync. This tool is present in the tool list for all installations, but full sync functionality is available via ThreatRecall.ai SaaS. When called against an OSS installation without the sync extension, the tool returns an error.

Input schema:

Property Type Required Default Description
limit integer no 20 Maximum objects to pull per STIX type

Request:

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "zettelforge_sync",
    "arguments": {
      "limit": 20
    }
  }
}

Response (OSS installation):

The tool returns an error response indicating that the sync extension is not installed. This is isError: false at the JSON-RPC level — the call succeeds; the error is inside the content text.

Response (ThreatRecall.ai SaaS):

Full sync results with per-type object counts. See the ThreatRecall.ai SaaS documentation for the exact response shape.


JSON-RPC methods

initialize

The client sends initialize as the first message to negotiate protocol version and discover server capabilities.

Request:

{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": {
      "name": "my-client",
      "version": "1.0.0"
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {
        "listChanged": false
      }
    },
    "serverInfo": {
      "name": "zettelforge",
      "version": "2.7.0"
    }
  }
}

notifications/initialized

Sent by the client after receiving the initialize response. The server does not send a response; it silently skips this message.

Request:

{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

tools/list

Return the full list of available tools with their input schemas.

Request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}

Response: Returns a tools array containing all 7 tool definitions, each with name, description, and inputSchema.

tools/call

Execute a named tool with the provided arguments.

Request:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "zettelforge_recall",
    "arguments": {
      "query": "APT28",
      "k": 5
    }
  }
}

Response (success):

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{ ... }"
      }
    ]
  }
}

Response (tool error):

Tool errors do not use JSON-RPC error codes. They are returned as successful JSON-RPC responses with isError: true in the result payload and the error message inside the text content:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"error\": \"Unknown tool: zettelforge_nonexistent\"}"
      }
    ],
    "isError": true
  }
}

Error codes

JSON-RPC standard error codes

Code Meaning When it occurs
-32700 Parse error Invalid JSON in request
-32600 Invalid request Request object is malformed
-32601 Method not found Unknown method (not initialize, tools/list, tools/call, or notifications/initialized)
-32602 Invalid params Tool arguments fail schema validation (handled by the MCP client; the server does not validate schemas)
-32603 Internal error Unhandled exception in the tool handler

Method-not-found response example

{
  "jsonrpc": "2.0",
  "id": 9,
  "error": {
    "code": -32601,
    "message": "Unknown method: does/not/exist"
  }
}

Tool-level errors

Scenario Error message
Unknown tool name "Unknown tool: {name}"
OpenCTI sync not available Error indicating the sync extension is not installed
OpenCTI sync failure Exception message from the sync extension
Backend connection failure Connection error from MemoryManager

Backward compatibility

Tool names prefixed with threatrecall_ (for example threatrecall_stats, threatrecall_remember) are transparently rewritten to zettelforge_* before dispatch:

if name.startswith("threatrecall_"):
    name = name.replace("threatrecall_", "zettelforge_", 1)

Existing agent workflows and configurations that reference the old naming continue to work without changes.

Implementation details

Server source location

File Purpose
src/zettelforge/mcp/server.py Core logic: TOOLS, handle_tool_call(), run_stdio(), get_mm()
src/zettelforge/mcp/__init__.py Public API re-export
src/zettelforge/mcp/__main__.py Entrypoint for python -m zettelforge.mcp

Module public API

from zettelforge.mcp import TOOLS, handle_tool_call, run_stdio
Symbol Type Description
TOOLS list[dict] Static tool definitions (7 tools) with input schemas
handle_tool_call(name, arguments) (str, dict) -> dict Route tool name and args to MemoryManager methods
run_stdio() () -> None Start the stdio-based JSON-RPC loop

Environment variables

Variable Default Description
ZETTELFORGE_BACKEND sqlite Storage backend: sqlite, jsonl, typedb, lancedb. Set at import time; ensures the server works out of the box.
ZETTELFORGE_HOME ~/.zettelforge Memory store directory

Test coverage

Unit tests are in tests/test_mcp_server.py and cover:

  • Lazy singleton contract (import does not instantiate MemoryManager)
  • initialize handshake response structure
  • tools/list returns all 7 tools with valid schemas
  • Unknown method returns JSON-RPC error code -32601
  • notifications/initialized produces no response
  • threatrecall_* backward-compatible name rewriting