Skip to content

Data Flow

This page describes the complete request flow from MCP client to SiYuan, including tool discovery, tool calling, CLI direct invocation, resource requests, error handling, and all other pathways.

Use case: You are debugging why a tool call succeeded/failed/results were filtered, or need to understand which checkpoints a request passes through.


1. Tool Discovery Flow (tools/list)

MCP Client
    ↓ ListToolsRequest
server.ts: ListToolsRequestSchema handler

getToolConfig()  [30s TTL cache / in-flight deduplication]
    ↓ Read from SiYuan storage /data/storage/petal/.../mcpToolsConfig
    ↓ normalizeToolConfig(raw)  [Three-format compatibility migration]
listAllTools(config)
    ↓ Iterate over 11 categories in TOOL_REGISTRY
    ↓ For each module: module.listTools(config[category])
        ↓ tools/internal/define-tool.ts: listTools()
            ↓ Filter enabled actions
            ↓ tools/internal/shared.ts: buildAggregatedTool(category, description, config, variants)
                ↓ Merge all enabled action schemas into single ToolDescriptor
                ↓ Inject common actions hint + dangerous warning into description
    ↓ Flatten and return ToolDescriptor[]
    ↓ Filter out tools where category enabled=false or all actions disabled
MCP Client ← ToolDescriptor[]

Key checkpoints:

  • config[category].enabled === false → entire tool is invisible
  • config[category].actions[action] === false → that action's schema is not merged into inputSchema
  • buildAggregatedTool detects all actions disabled → returns empty array (tool filtered out)

2. Tool Call Flow (tools/call)

2.1 Complete Call Chain

MCP Client
    ↓ CallToolRequest { name: "notebook", arguments: { action: "list" } }
server.ts: CallToolRequestSchema handler

Step 1: Parse tool name
    resolveCategory("notebook") → "notebook"
    getToolConfig() → config
    If config.notebook.enabled === false
        → Return "Tool \"notebook\" is disabled."

Step 2: Lifecycle wrapping
    runToolCall(ctx, handler)
        ctx = { client, category: "notebook", name: "notebook",
               action: "list", args: { action: "list" }, requestText: JSON.stringify(args) }

    Step 2a: Puppy preparation
        earnPuppyBalance()  (+1)
        writePuppyEvent({ status: "running", tool: "notebook", action: "list" })

    Step 2b: Business handler
        TOOL_REGISTRY["notebook"].callTool(client, args, config.notebook, permMgr)

        defineTool factory:
            tryHandleHelpAction() → No (action is not "help")
            actionSchema.parse({ action: "list" }) → Passes Zod validation
            Check config.notebook.actions["list"] === true → Passes
            handler({ client, rawArgs: { action: "list" }, permMgr })

            src/api/notebook.ts: listNotebooks(client)

            SiYuanClient.request("/api/notebook/lsNotebooks")

            SiYuan HTTP API → Returns notebook list

            Build ToolResult { content: [...] }

        Return ToolResult

    Step 2c: Puppy cleanup
        writePuppyEvent({ status: "success", ... })

    Step 2d: Analytics
        buildAnalyticsEvent()
            measureApproxText(requestText) → request tokens
            measureApproxContent(result.content) → response tokens
            estimateResultSizeHint() → '0' / '0-200' / '200-1K' / ...
        persistAnalyticsEvent()  [await in CLI, fire-and-forget otherwise]
        maybeSendTelemetry()  [fire-and-forget]

    Return ToolResult
MCP Client ← ToolResult

2.2 Permission Checkpoint

For operations involving notebook-scoped data, permission checks happen inside the business handler:

Handler execution

tools/internal/context.ts: ensurePermissionForDocumentId(client, permMgr, id, "write")

resolveDocumentContextById(client, id) → Query SQL/API for notebookId

checkPermissionForDocumentContext(permMgr, notebookId, "write")

permMgr.canWrite(notebookId) → Check permission file

If insufficient → return createPermissionDeniedResult()
    Otherwise → Continue business logic

Note: Permission checks are not unified in server.ts or tool-lifecycle.ts. Each tool handler explicitly calls them according to business needs (e.g. list only needs r, remove needs rwd).

2.3 Error Handling Chain

Business handler throws error

defineTool factory catch

createErrorResult(error, context)
    ├─ ZodError → { type: "validation_error", message: "Invalid parameters: ..." }
    ├─ SiYuanError → { type: "api_error", code: error.code, message: error.msg }
    └─ Other Error → { type: "internal_error", message: error.message }

Return ToolResult containing error

tool-lifecycle.ts: runToolCall catch
    buildAnalyticsEvent(status="error")
    persistAnalyticsEvent()
    maybeSendTelemetry()
    throw error (continue upward)

server.ts: CallToolRequestSchema handler catch
    Return MCP Error response

Error classification:

TypeTrigger conditionClient experience
validation_errorZod schema validation failsParameter error hint
api_errorSiYuan HTTP API returns code ≠ 0API error (includes SiYuan error code)
disabled_errorTool or action is disabled"Tool/Action is disabled"
permission_deniedInsufficient notebook permission"Permission denied for notebook ..."
internal_errorUnexpected exceptionInternal error message

3. CLI Direct Invocation Flow

CLI bypasses MCP protocol and directly reuses core logic:

Terminal: siyuan-sisyphus notebook list

cli/index.ts: parseArgs(["notebook", "list"])

cli/args.ts: minimist first pass
    → command = "dispatch", tool = "notebook", action = "list"
    → extractToolRest() → rest = []

cli/dispatch.ts: runDispatch()

Step 1: Validation
    resolveCategory("notebook") → Exists ✓
    "list".replace(/-/g, "_") → "list"
    ACTIONS_BY_CATEGORY["notebook"].includes("list") → Exists ✓

Step 2: Config resolution
    loadFileConfig() → Read ~/.siyuan-sisyphus/config.json
    resolveConfig({ cliUrl, cliToken, profile }) → Merge by priority
    applyConfigToEnv() → Set SIYUAN_API_URL / SIYUAN_TOKEN

Step 3: Client & permissions
    new SiYuanClient(baseUrl) + setToken(token)
    ensureRequiredPluginInstalled(client) → Read plugin.json to verify plugin exists
    new PermissionManager(client).load()

Step 4: Tool config
    read /data/storage/petal/.../mcpToolsConfig → normalizeToolConfig()

Step 5: Flag mapping
    TOOL_REGISTRY["notebook"].listTools(toolConfig.notebook) → Get inputSchema
    mapFlagsToArgs([], inputSchema) → {}  [No extra flags]
    Assemble payload: { action: "list" }

Step 6: Execute (reuses plugin core)
    runToolCall(ctx, () => TOOL_REGISTRY["notebook"].callTool(client, payload, config, permMgr))
        ↓ Identical lifecycle + handler path as plugin mode
    Return ToolResult

Step 7: Render
    renderToolResult(result, { json: false, debug: false })
        → Human-readable: green ✓ + notebook list
    If TTY and multi-page → runInteractivePaging()

Terminal output

CLI vs Plugin Mode Differences:

DifferencePlugin ModeCLI Mode
Outer protocolMCP stdio/HTTPDirect function calls
Config sourceSiYuan storage (UI-controlled)~/.siyuan-sisyphus/config.json
Tool togglesUser fine-controlledSame UI-controlled config
Dangerous actionsRelies on LLM self-disciplineUser typing command is confirmation
AnalyticsFire-and-forgetSynchronously flushed
Permission defaultUnconfigured notebook defaults to r (read-only)Same PermissionManager behavior
Result renderingMCP JSONHuman-readable / --json

4. Resource Request Flow (resources/read)

MCP Resources provide static help docs and dynamic action help, bypassing normal tool calls.

4.1 Static Resources

MCP Client
    ↓ ListResourcesRequest
server.ts: ListResourcesRequestSchema handler
    ↓ resources.ts: listHelpResources()
Return static resource list:
    - siyuan://help/tool-overview
    - siyuan://help/path-semantics
    - siyuan://help/examples
    - siyuan://help/ai-layout-guide

MCP Client
    ↓ ReadResourceRequest { uri: "siyuan://help/tool-overview" }
server.ts: ReadResourceRequestSchema handler
    ↓ resources.ts: readHelpResource(uri)
        → Match URI prefix → Return predefined Markdown help text

4.2 Dynamic Resource Templates

MCP Client
    ↓ ListResourceTemplatesRequest
server.ts: ListResourceTemplatesRequestSchema handler
    ↓ resources.ts: listHelpResourceTemplates()
Return resource templates:
    - siyuan://help/action/{tool}/{action}

MCP Client
    ↓ ReadResourceRequest { uri: "siyuan://help/notebook/list" }
server.ts: ReadResourceRequestSchema handler
    ↓ resources.ts: readHelpResource(uri)
        → Parse tool="notebook", action="list"
        → Call corresponding tool's help generation logic
        → Return detailed help text for that action

Dynamic help content sources:

  • Guidance and action hints from help.ts
  • VARIANTS definitions in each tool module (accepted shapes, required fields, example)
  • presentation/invocation-format.ts automatically translates to CLI equivalent (used in CLI help subcommand)

5. HTTP Transport Special Flows

5.1 Session Management

AI Client → POST /mcp (initialize request, no mcp-session-id)
http-transport.ts → Create new Session → Generate session-id
                  → Store in Map<string, SessionEntry>
                  → Response header Set-Cookie: mcp-session-id=xxx
                  → Return 200 + JSON-RPC response

Subsequent requests → POST /mcp (with mcp-session-id header)
         → Lookup SessionEntry from Map
         → Reuse same Server instance
         → Return response

5.2 Auth Flow

AI Client → POST /mcp
         → Authorization: Bearer <token>
http-transport.ts → Read SIYUAN_MCP_TOKEN env var
                  → Compare with Bearer token from request
                  → Mismatch → Return 401 Unauthorized
                  → Match → Continue processing

5.3 Parent Watchdog

On HTTP server startup, read SIYUAN_MCP_PARENT_PID
         → Set timer to check if that PID is still running
         → If parent exited → Call server.shutdown() → Process exits

Purpose: When the SiYuan main process crashes or closes, the embedded HTTP MCP server automatically cleans up to avoid zombie processes.


6. Config Loading & Migration Flow

6.1 Plugin Mode Config Loading

Plugin onload()

loadPersistedToolConfig() [tool-config-storage.ts]
    ↓ loadData("mcpToolsConfig")
    If exists → normalizeToolConfig(raw) [Three-format compatibility]
           → Check: if category enabled=true but all actions=false → Auto set enabled=false
           → Return ToolConfig
    If not exists → buildDefaultToolConfig()
             → savePersistedToolConfig() [First-time write of defaults]

Save to plugin instance variable

6.2 CLI Config Loading

CLI startup

loadFileConfig() [cli/config.ts]
    ↓ Read ~/.siyuan-sisyphus/config.json (compatible with old path)
    If exists → Parse JSON → Return config object
    If not exists → Return null

resolveConfig(options)
    apiUrl  = options.cliUrl || env.SIYUAN_API_URL || activeProfile.apiUrl || DEFAULT_API_URL
    token   = options.cliToken || env.SIYUAN_TOKEN || activeProfile.token || ''

7. Analytics & Telemetry Data Flow

7.1 Analytics Event Write

Tool call completes (success or failure)

tool-lifecycle.ts: buildAnalyticsEvent()
    ├─ timestamp
    ├─ transport: "stdio" | "http" | "cli"
    ├─ category, action
    ├─ duration_ms
    ├─ status: "success" | "error"
    ├─ request_approx_tokens (chars/4)
    ├─ response_approx_tokens (chars/4)
    ├─ result_size_hint: "0" | "0-200" | "200-1K" | "1K-10K" | ">10K"
    ├─ error_type (if any)
    └─ error_code (if any)

analytics.ts: appendAnalyticsEvent()
    Read existing analytics JSONL file
    Append new event line
    If file > 2MB → Rotate (rename to .1, create new)
    Write to SiYuan storage

If transport === "cli" → await write completion
Otherwise → fire-and-forget (errors swallowed by .catch)

7.2 Telemetry Report

tool-lifecycle.ts: maybeSendTelemetry()

telemetry.ts: Check last report time
    If interval insufficient → Skip
    If interval sufficient → Read analytics events → Build aggregated payload
        ├─ total_calls
        ├─ success_rate
        ├─ avg_duration_ms
        ├─ top_actions[]
        ├─ transport_distribution
        └─ error_rate

POST to telemetry endpoint (configured by telemetryConfig.endpoint)

Fire-and-forget (.catch swallows errors, does not affect main flow)

8. Puppy Mascot State Flow

MCP Server (tool-lifecycle.ts)
    ↓ writePuppyEvent()
puppy-state.ts: Write puppyEvents.json

ToolPuppy.svelte (via puppy-polling.ts)
    ↓ Poll every 500ms POST /api/file/getFile
Read puppyEvents.json

Parse event array → Take latest event

State machine transition:
    status="running" → Set corresponding tool icon + action marker + animation
    status="success" → Show ✓ green badge + heart burst (probabilistic)
    status="error"   → Show ✗ red badge + sad eyes
    status="dangerous" → Show ⚠ exclamation + dangerous eyes

Idle 3 seconds → Return to idle state

puppy-motion.ts: Random idle action scheduling (stand/sit/look/groom/lie/sleep)

Decoupled design: Puppy UI and MCP Server do not directly share JS objects/memory. They communicate through filesystem event files. This allows Puppy to run independently of the server (e.g. in test mode without a backend).


9. Complete Request Lifecycle Sequence

AI Client          MCP Server          Tool Lifecycle      Tool Handler        SiYuan API
   |                    |                     |                   |                  |
   |──ListTools───────→|                     |                   |                  |
   |                    |──getToolConfig()───→|                   |                  |
   |                    |←─ToolConfig─────────|                   |                  |
   |                    |──listAllTools()────→|                   |                  |
   |                    |                    |──defineTool.listTools()            |
   |                    |                    |←─ToolDescriptor[]                  |
   |←─ToolDescriptor[]──|                    |                   |                  |
   |                    |                     |                   |                  |
   |──CallTool─────────→|                     |                   |                  |
   |                    |──resolveCategory()──→|                   |                  |
   |                    |←─category───────────|                   |                  |
   |                    |──runToolCall()──────→|                   |                  |
   |                    |                    |──earnPuppyBalance()                 |
   |                    |                    |──writePuppyEvent("running")         |
   |                    |                    |──handler()────────→|                  |
   |                    |                    |                   |──actionSchema.parse()           |
   |                    |                    |                   |──SiYuanClient.request()───────→|
   |                    |                    |                   |←─SiYuanResponse────────────────|
   |                    |                    |                   |──build ToolResult              |
   |                    |                    |←─ToolResult───────|                  |
   |                    |                    |──writePuppyEvent("success")         |
   |                    |                    |──appendAnalyticsEvent()            |
   |                    |                    |──maybeSendTelemetry()              |
   |                    |←─ToolResult────────|                   |                  |
   |←─ToolResult────────|                    |                   |                  |

Released under the MIT License.