| title | MCP Protocol Reference | ||||||
|---|---|---|---|---|---|---|---|
| description | Complete reference for the ZettelForge MCP server: tool schemas, JSON-RPC 2.0 protocol details, request/response examples, error codes, and the lazy-singleton lifecycle. | ||||||
| diataxis_type | reference | ||||||
| audience | Tool integrators, MCP client developers, LLM agent framework authors | ||||||
| tags |
|
||||||
| last_updated | 2026-04-27 | ||||||
| version | 2.0.0 |
The ZettelForge MCP server implements the Model Context Protocol specification (protocol version 2024-11-05) over stdio transport using JSON-RPC 2.0.
- Transport: stdio (stdin for requests, stdout for responses)
- Protocol: JSON-RPC 2.0
- MCP version:
2024-11-05 - Server name:
zettelforge - Lazy initialization:
MemoryManageris instantiated on first tool call, not on import
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 ---------------|
- Importing
zettelforge.mcp(orzettelforge.mcp.server) does not instantiateMemoryManager. - The
initializeandtools/listmethods work without touching the backend. MemoryManageris created on the firsttools/callthat reacheshandle_tool_call().- This makes tool introspection side-effect-free for clients that only need the tool list.
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()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 decide whether to add, update, or supersede.
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",
"evolve": true
}
}
}Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"note_id\": \"abc123-def456\",\n \"status\": \"created\",\n \"entities\": [\"apt28\", \"cve-2024-3094\"]\n}"
}
]
}
}Input schema:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
content |
string | yes | — | Threat intelligence text to store |
domain |
string | no | "cti" |
Domain: cti, incident, general |
source |
string | no | "mcp" |
Source reference string |
evolve |
boolean | no | true |
Enable memory evolution (LLM-based dedup) |
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 |
Search memory using blended vector + graph retrieval. Returns ranked results with entities, confidence scores, and tier metadata.
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:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"results\": [\n {\n \"id\": \"note-123\",\n \"content\": \"APT28 deployed Cobalt Strike beacons against NATO-aligned government networks in Q1 2026...\",\n \"context\": \"actor:apt28 | tool:cobalt strike\",\n \"entities\": [\"apt28\", \"cobalt strike\"],\n \"tier\": \"verified\",\n \"confidence\": 0.92\n }\n ],\n \"count\": 1,\n \"latency_ms\": 42\n}"
}
]
}
}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 |
Response fields:
| Field | Type | Description |
|---|---|---|
results |
object[] | Ranked search results |
results[].id |
string | Note ID |
results[].content |
string | First 500 characters of note content |
results[].context |
string | Semantic context string |
results[].entities |
string[] | Up to 10 extracted entities |
results[].tier |
string | Epistemic tier: verified, reported, inferred |
results[].confidence |
number | Confidence score (0.0 to 1.0) |
count |
integer | Number of results returned |
latency_ms |
integer | Query latency in milliseconds |
Generate a synthesized answer from ZettelForge memories using RAG (Retrieval-Augmented Generation). Supports multiple output formats.
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "zettelforge_synthesize",
"arguments": {
"query": "Describe the relationship between APT28 and Lazarus Group",
"format": "relationship_map"
}
}
}Response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"synthesis\": {\n \"answer\": \"Based on stored intelligence, APT28 and Lazarus Group are distinct North Korean and Russian state-sponsored threat actors respectively...\",\n \"format\": \"relationship_map\"\n },\n \"sources_count\": 4\n}"
}
]
}
}Input schema:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
query |
string | yes | — | Question to answer from memory |
format |
string | no | "direct_answer" |
Output format: direct_answer, synthesized_brief, timeline_analysis, relationship_map |
Response fields:
| Field | Type | Description |
|---|---|---|
synthesis |
object | The generated answer (shape varies by format) |
sources_count |
integer | Number of memory notes used as sources |
Fast entity lookup by type. Uses an O(1) index for direct entity-to-note mapping.
Request:
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "zettelforge_entity",
"arguments": {
"type": "cve",
"value": "CVE-2024-3094",
"k": 5
}
}
}Response:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"results\": [\n {\n \"id\": \"note-456\",\n \"content\": \"CVE-2024-3094 is a critical backdoor in xz-utils discovered in March 2024...\",\n \"tier\": \"verified\"\n }\n ],\n \"count\": 1\n}"
}
]
}
}Input schema:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | yes | — | Entity type: actor, cve, tool, campaign, person, location |
value |
string | yes | — | Entity value (e.g. "apt28", "CVE-2024-3094") |
k |
integer | no | 5 |
Maximum results |
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 |
results[].tier |
string | Epistemic tier |
count |
integer | Number of results |
Traverse the STIX 2.1 knowledge graph starting from a given entity. Shows relationships such as uses, targets, attributed-to.
Request:
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "zettelforge_graph",
"arguments": {
"type": "actor",
"value": "apt28",
"max_depth": 2
}
}
}Response:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"paths\": [\n [\n {\"from\": \"apt28\", \"rel\": \"uses\", \"to\": \"cobalt strike\"},\n {\"from\": \"cobalt strike\", \"rel\": \"targets\", \"to\": \"windows\"}\n ]\n ],\n \"count\": 1\n}"
}
]
}
}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 |
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 |
Return memory system statistics including version, total note count, retrieval count, and entity index breakdown.
Request:
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "zettelforge_stats",
"arguments": {}
}
}Response:
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"version\": \"2.0.0\",\n \"total_notes\": 142,\n \"retrievals\": 3891,\n \"entity_index\": {\n \"actor\": 12,\n \"cve\": 34,\n \"tool\": 28,\n \"campaign\": 7,\n \"person\": 3,\n \"location\": 9\n }\n}"
}
]
}
}Input schema: No required or optional parameters.
Response fields:
| Field | Type | Description |
|---|---|---|
version |
string | ZettelForge version string |
total_notes |
integer | Total number of stored notes |
retrievals |
integer | Cumulative retrieval count |
entity_index |
object | Entity type counts (keys vary by memory contents) |
Trigger a sync from OpenCTI. Pulls the latest reports, indicators, threat actors, malware, and vulnerabilities. Requires the zettelforge-enterprise package.
Request:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "zettelforge_sync",
"arguments": {
"limit": 20
}
}
}Response (success):
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"synced\": {\n \"reports\": 5,\n \"indicators\": 20,\n \"threat_actors\": 3,\n \"malware\": 8,\n \"vulnerabilities\": 4\n },\n \"errors\": []\n}"
}
]
}
}Response (enterprise not installed):
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"error\": \"OpenCTI sync requires the zettelforge-enterprise package.\"\n}"
}
]
}
}Input schema:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
limit |
integer | no | 20 |
Maximum objects to pull per STIX type |
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.0.0"
}
}
}Sent by the client after receiving the initialize response. The server does not send a response.
Request:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}The server silently skips this message (no response written to stdout).
Return the full list of available tools with their input schemas.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "zettelforge_remember",
"description": "Store threat intelligence in ZettelForge memory...",
"inputSchema": {
"type": "object",
"properties": { ... }
}
}
]
}
}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, e.g. tool raised an exception):
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "{\"error\": \"Connection to TypeDB failed\"}"
}
],
"isError": true
}
}| 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 sent (not initialize, tools/list, tools/call, or notifications/initialized) |
-32602 |
Invalid params | Tool arguments fail schema validation (handled by the MCP client; server does not validate schemas) |
-32603 |
Internal error | Unhandled exception in tool handler |
{
"jsonrpc": "2.0",
"id": 9,
"error": {
"code": -32601,
"message": "Unknown method: does/not/exist"
}
}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": 10,
"result": {
"content": [
{
"type": "text",
"text": "{\"error\": \"Unknown tool: zettelforge_nonexistent\"}"
}
],
"isError": true
}
}Tool-level error scenarios:
| Scenario | Error message |
|---|---|
| Unknown tool name | "Unknown tool: {name}" |
| OpenCTI sync without enterprise | "OpenCTI sync requires the zettelforge-enterprise package." |
| OpenCTI sync failure | "{exception message}" (passthrough from enterprise package) |
| Backend connection failure | "Connection to ... failed" (from MemoryManager) |
Tool names prefixed with threatrecall_ (e.g. threatrecall_stats, threatrecall_remember) are transparently rewritten to zettelforge_* before dispatch. The server applies this rewrite:
if name.startswith("threatrecall_"):
name = name.replace("threatrecall_", "zettelforge_", 1)This ensures existing agent workflows and configurations that reference the old naming continue to work without changes.
The MCP server is implemented entirely in:
src/zettelforge/mcp/server.py— Core logic:TOOLS,handle_tool_call(),run_stdio(),get_mm()src/zettelforge/mcp/__init__.py— Public API re-exportsrc/zettelforge/mcp/__main__.py— Entrypoint forpython -m zettelforge.mcp
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 |
| Variable | Default | Description |
|---|---|---|
ZETTELFORGE_BACKEND |
sqlite |
Storage backend: sqlite, jsonl, typedb, lancedb |
ZETTELFORGE_HOME |
~/.zettelforge |
Memory store directory |
The backend environment variable is set automatically to sqlite if no other value is provided, ensuring the server works out of the box without configuration.
Unit tests are in tests/test_mcp_server.py and cover:
- Lazy singleton contract (import does not instantiate MemoryManager)
initializehandshake response structuretools/listreturns all 7 tools with valid schemas- Unknown method returns JSON-RPC error code
-32601 notifications/initializedproduces no responsethreatrecall_*backward-compatible name rewriting