Skip to content

Design Decisions ​

This page documents major architectural decisions and the trade-offs behind them. Each decision includes problem context, choice made, rejected alternatives, and current outcome.

Use case: You are judging whether a change aligns with the current design direction, or need to understand why something was designed the way it is.


1. Aggregated Tool Design ​

Problem Context ​

SiYuan provides approximately 459 HTTP API endpoints. Exposing one MCP tool per endpoint would create a surface of 100+ tools, leading to:

  • Exploding context cost: Each MCP list_tools returning 100+ descriptors consumes massive system prompt tokens
  • Poor discoverability: LLMs struggle to choose correctly from 100+ tools
  • Naming collisions: Many similar names (listNotebooks / listDocs / listBlocks) cause confusion

Choice Made ​

Aggregate related APIs by domain into 13 MCP tools:

MCP ToolSiYuan Domain CoveredAction Count
fsFilesystem-style document operations7
notebookNotebook CRUD~10
documentDocument tree operations~17
blockBlock-level operations~21
avAttribute View (database)~13
fileFiles & assets~11
searchSearch & query~11
tagTag management~3
systemSystem & UI~10
flashcardFlashcard review~8
extensionOfficial plugin tools and optional native SiYuan MCP toolsDynamic
mascotMascot interaction~3
feedbackProduct feedback submission1

Each tool distinguishes specific operations via the action parameter, e.g.:

notebook(action="list")
notebook(action="create")
notebook(action="rename")

Rejected Alternatives ​

  • Option A: One API, One Tool: Each SiYuan API maps to one MCP tool. Rejected: context cost too high, LLM selection difficulty.
  • Option B: Hide action layer completely: Only expose a small set of tools, with actions as internal implementation details. Rejected: LLM needs to know available operations, and different actions have very different parameters that cannot be hidden under a unified schema.
  • Option C: Dynamically show by frequency: Only show relevant tools based on context. Rejected: MCP protocol currently has no dynamic list_tools mechanism; would require complex server-side state machine.

Current Outcome ​

  • MCP tool surface reduced from 100+ to 12
  • list_tools response size reduced from ~50KB to ~8KB
  • Significantly improved LLM tool discoverability
  • Each action's parameters are strictly validated via Zod schema, reducing error rates

2. Progressive Disclosure ​

Keep only routing and common call contracts at mount time ​

Keep the 14 aggregated tools and their existing action API. Do not split each action into a separate tool.

LayerResponsibility
Initialize instructionsCross-tool routing, read/edit/verify workflow, partial-read limits, user rules/memory priority, confirmation and strict-write preflight
tool.descriptionSelection criteria, path/ID semantics, edit scope, compact action(required fields) signatures; * marks confirmation-required actions
inputSchemaEnabled action enum, field types and constraints, one description per field; defer large nested shapes
action="help", topic="<action>"Complete original parameter schema, alternative required fields, examples, domain guidance and confirmation requirements
siyuan://help/action/{tool}/{action}The same full parameter schema and Markdown help; use the help action when resources are unavailable
siyuan://skills/* and docsMulti-step workflows, layout/domain guidance and full reference

Global rules appear once. App handoff rules stay on the relevant App descriptors and responses. Merged fields no longer concatenate every action's description or repeat both parameter-contract and required-by lists. Compatibility aliases remain accepted at runtime while discovery favors canonical names.

Nested merged schemas exceeding 600 characters are reduced to container types and help pointers in tools/list only. Short shapes, scalar types and enums remain. Original action schemas, Zod validation, internal CLI action branches, write preflight and permissions remain intact. Help returns the full schema root, preserving required, composition constraints and reference targets. Complex calls should fetch action help first; this adds a help round trip for those tasks.

Call workflow and help entrypoints ​

Prompt rules use explicit conditions and next steps:

  • Read action help before using an unfamiliar action or constructing a collapsed nested value; reuse help already read during the task. The merged schema contains fields for multiple actions, not one universal call shape.
  • Read the target and nearby blocks before editing. Prefer exact replacement for local changes; verify affected content after structural or multi-part edits.
  • Follow returned continuation fields when more context is needed. Search snippets, partial reads and lossy representations cannot establish that original content is absent.
  • Keep path formats, block scope and AV identifier distinctions next to their tools. Read AV schema and resolved row IDs before editing database values.
  • Strict-write execution keeps business arguments unchanged, sets validateOnly=false, and copies the returned credentials. Preflight success alone does not execute the write.

The layout guide is also available through action="help", topic="ai-layout-guide", so clients without resource support can read the same specification. Regression tests follow collapsed-field help pointers through both routes and check their complete schemas without reading notes.

Measurement and regression budgets ​

Run npm run analyze:mcp (or pnpm analyze:mcp). The script loads the actual registry and measures instructions.trim().length + JSON.stringify({ tools }).length; it no longer maintains a duplicate hard-coded tool catalog. Pass --root PATH to measure another source tree.

Default configurationBefore (227985d)Compact version (d49848a)With workflow guidanceShorter repeated field descriptions
Instructions20,988 chars3,216 chars3,754 chars3,754 chars
tools/list (14 tools)133,583 chars57,720 chars58,344 chars56,531 chars
Total154,571 chars60,936 chars62,098 chars60,285 chars
Approximate tokens (chars / 4)38,64315,23415,52515,072
AV tool within the total42,515 chars11,231 chars11,465 chars11,304 chars

Total reduction from before compaction: 61.0%. Shortening repeated action, topic, validateOnly, requestId and hash descriptions saves 1,813 characters (2.9%) compared with the workflow-guidance version. Schema description text falls from 19,485 to 17,672 characters. Full global instructions and action contracts stay unchanged; field descriptions still identify server-issued credentials, unchanged reuse and preflight without mutation. Types, enums and validation constraints are preserved.

Scope: default config, no custom user rules or memory, no dynamically discovered third-party tools or optional MCP Apps. Additional client-visible descriptors cost extra. Tokens are estimates, not measurements with a Claude/OpenAI tokenizer. User rules and memory are never truncated to fit the budget.

tests/unit/core/mcp-payload.test.ts caps default payloads at 64,000 characters and all-static-actions payloads at 66,000, with separate instructions/AV limits. It also checks full help, nested validation and disabled actions. Run npm test to verify.

Sources and limits ​

  • OpenAI metadata guidance: describe tool purpose and scope precisely, document arguments and constrained values, and use truthful annotations.
  • OpenAI Tool search: clients can defer tool loading to reduce persistent context. defer_loading is a client API option, not a portable MCP server field.

This project implements disclosure through its existing help/resources for client compatibility. The 600-character threshold and payload budgets are project decisions, not official MCP requirements. Real-model A/B error-rate evaluation has not been performed; unit/integration tests verify protocol, parameter and safety behavior.


3. Permission Model ​

Problem Context ​

When external AI Agents connect to SiYuan, controllable data access boundaries are needed:

  • Some notebooks may contain sensitive information
  • AI should not be able to arbitrarily delete or modify important data
  • Permission control needs moderate granularity (too fine is hard to manage, too coarse offers no protection)

Choice Made ​

Adopt notebook-level 4-tier permissions:

LevelPermissionUse Case
noneCompletely blockedSensitive notebooks
rRead-onlyReference notebooks
rwRead-write (no delete)Daily work notebooks
rwdFull permissionTrusted areas

Implementation details:

  • Permission file stored at /data/storage/petal/siyuan-plugins-mcp-sisyphus/notebookPermissions
  • Read/write via SiYuan API, never directly accessing local filesystem
  • Unconfigured notebooks default to r (read-only) so missing permission entries do not grant write/delete access
  • Permission checks are explicitly called by business handlers, not unified middleware (different actions have different needs)

Rejected Alternatives ​

  • Option A: Document-level permissions: Too fine-grained, high management cost, and SiYuan natively does not support document-level permissions.
  • Option B: Action-level permissions (each action individually toggleable): Already exists (ToolConfig's actions), but this is a feature toggle, not a security boundary. Security boundaries need to be on the data dimension (notebook).
  • Option C: Global read-only mode: Too blunt, cannot satisfy the need for some notebooks writable and some not.

Current Outcome ​

  • Settings panel provides Notebook permission matrix UI
  • Permission validation occurs before API calls, blocking unauthorized operations
  • CLI mode defaults to full open if permission file cannot be read (CLI user typing command is considered confirmation)

4. Plugin & CLI Shared Core ​

Problem Context ​

The project needs to support two usage patterns:

  1. Plugin mode: AI clients communicate with the SiYuan plugin via MCP protocol
  2. CLI mode: Users execute commands directly in the terminal

Independent implementations would cause code duplication, inconsistent behavior, and doubled maintenance cost.

Choice Made ​

CLI directly imports plugin source code, sharing the following core modules:

Shared layers:
├── src/api/client.ts           SiYuanClient
├── src/core/tool-registry.ts    TOOL_REGISTRY
├── src/core/tool-lifecycle.ts   runToolCall (puppy/analytics/telemetry)
├── src/core/config.ts           buildDefaultToolConfig, ACTIONS_BY_CATEGORY
├── src/core/permissions.ts      PermissionManager
├── src/tools/*/index.ts          All tool implementations
└── src/shared/invocation-format.ts  Dual-mode presentation unification

Layers CLI does NOT use:
├── @modelcontextprotocol/server Does not start MCP server
├── src/core/server.ts           Skips ListTools/CallTool handlers
├── src/core/http-transport.ts   Does not start HTTP server
├── src/core/resources.ts        Does not expose MCP Resources
├── src/core/server-instructions.ts  No instructions
└── src/index.ts                Skips plugin lifecycle

Rejected Alternatives ​

  • Option A: CLI spawns child MCP server: Attempted in early versions. Rejected: complex process management, slow startup, resource waste, difficult debugging.
  • Option B: CLI fully independent implementation: Rejected: severe code duplication; tool logic changes would need to be synced in two places.

Current Outcome ​

  • CLI output cli.cjs is a self-contained bundle with no node_modules dependency
  • Tool bug fixes only need to change one place (src/tools/), fixing both plugin and CLI simultaneously
  • CLI behavior is 100% consistent with the plugin (except config source and tool toggle defaults)

5. Transport Layer Choice ​

Problem Context ​

The MCP protocol supports multiple transport methods. The most suitable one needs to be chosen for each usage scenario.

Choice Made ​

Support stdio (default) and HTTP/S transports:

TransportImplementationUse Case
stdioSDK v2 serveStdio() with legacy serving enabledLocal AI clients across both protocol eras
HTTPSDK v2 request classifier + modern/legacy handlersRemote access, browsers, multi-client sharing

HTTP mode enhancements:

  • Dual-era negotiation: MCP 2026-07-28 requests use a strict stateless handler; legacy requests retain independent sessions
  • Input hardening: Validates Origin and JSON content type before protocol dispatch
  • Bearer Token auth: Prevents unauthorized access
  • TLS support: Encrypted transport for production
  • Parent Watchdog: Auto-cleanup when SiYuan main process exits

Rejected Alternatives ​

  • Option A: stdio only: Cannot satisfy remote access and browser scenarios.
  • Option B: Replace the endpoint with a modern-only handler: Rejected because existing clients still negotiate older protocol revisions.
  • Option C: WebSocket transport: MCP protocol has not standardized WebSocket transport; poor compatibility.

Current Outcome ​

  • stdio mode works out of the box with zero configuration
  • HTTP mode provides a complete configuration panel; users can customize host/port/token/TLS
  • Both modes can be switched with one click in the settings panel

6. CLI Config Priority ​

Problem Context ​

CLI needs to support multiple config sources with clear conflict resolution priority.

Choice Made ​

Priority from high to low:
1. CLI flag        (--url / --token)
2. Environment variable (SIYUAN_API_URL / SIYUAN_TOKEN)
3. Config file     (active profile in ~/.siyuan-sisyphus/config.json)
4. Default         (http://127.0.0.1:6806)

Multi-profile support:

  • config.json contains profiles: Record<string, { apiUrl, token }>
  • currentProfile field indicates the default active profile
  • --profile <name> can temporarily switch

Rejected Alternatives ​

  • Option A: Config file only: Inconvenient for scripting and CI/CD.
  • Option B: Environment variable highest priority: Inconvenient for users to temporarily override (e.g. testing different endpoints).
  • Option C: No profile concept: Poor experience when managing multiple environments (local/remote/work/personal).

Current Outcome ​

  • Scripting: siyuan-sisyphus block list --url http://remote:6806 --token xxx
  • CI/CD integration: SIYUAN_API_URL=... siyuan-sisyphus ...
  • Daily development: Configure once with siyuan-sisyphus config set default --url http://127.0.0.1:6806, then call directly

7. Build Design ​

Problem Context ​

The project needs to produce three artifacts (plugin UI, MCP server, CLI) with different tech stacks (browser vs Node.js environment).

Choice Made ​

Vite multi-entry configuration:

BUILD_TARGET=renderer  →  dist/index.js       (Browser environment, Svelte UI)
BUILD_TARGET=server    →  dist/mcp-server.cjs (Node.js environment, MCP Server)
BUILD_TARGET=cli       →  cli/dist/cli.cjs    (Node.js environment, Standalone CLI)

Key build decisions:

DecisionDescription
Output formatAll CommonJS (CJS), compatible with SiYuan plugin loading mechanism
inlineDynamicImportsForce inline dynamic imports, single-file output
server/cli externalPreserve Node built-in modules (fs/path/http etc.), do not bundle
renderer externalOnly exclude siyuan (injected by SiYuan runtime)
CLI shebangInject #!/usr/bin/env node header, chmod 755
SDK lightweightCustom rollup plugin replaces validation/ajv-provider.js and experimental/tasks/* with local noop implementations to reduce bundle size

Rejected Alternatives ​

  • Option A: Direct tsc compilation: Cannot control bundle size, no tree-shaking or noop replacement.
  • Option B: Direct esbuild / rollup: Vite already provides out-of-the-box TypeScript + Svelte support; no need to reconfigure.
  • Option C: Separate package.json and build flow per artifact: Too high maintenance cost; Vite multi-entry is flexible enough.

Current Outcome ​

  • pnpm dev simultaneously watches renderer + server
  • pnpm build produces dist/index.js + dist/mcp-server.cjs + package.zip
  • pnpm build:cli produces cli/dist/cli.cjs (self-contained, zero dependencies)
  • Artifact sizes: index.js ~30KB, mcp-server.cjs ~284KB, cli.cjs ~(self-contained)

8. Error Handling Strategy ​

Problem Context ​

The system needs to handle errors from multiple sources: Zod validation, SiYuan API, network timeouts, insufficient permissions, config anomalies, etc. Different errors need to be presented differently to different consumers (LLM vs human terminal users).

Choice Made ​

Unified error formatting (tools/internal/shared.ts: createErrorResult):

ZodError          → type: "validation_error",  message: "Invalid parameters: ..."
SiYuanError       → type: "api_error",         code: siYuanCode, message: siYuanMsg
Permission denied → type: "permission_denied", message: "Permission denied for notebook ..."
Disabled tool/action → type: "disabled_error", message: "Tool/Action is disabled"
Other Error       → type: "internal_error",    message: error.message

Presentation layer unification (presentation/invocation-format.ts):

  • MCP mode: Error text maintains tool(action="...") style
  • CLI mode: Error text automatically translates to siyuan <tool> <action> --flag style

Rejected Alternatives ​

  • Option A: Throw raw Error directly to MCP SDK: Would cause LLM to receive unfriendly stack traces.
  • Option B: Each error formats independently: Hard to maintain, inconsistent style.

Current Outcome ​

  • LLM receives structured error information and can auto-correct parameters
  • CLI users receive human-readable error hints with field-level validation details
  • All error types have explicit type fields for client-side classification

9. Puppy Mascot Architecture ​

Problem Context ​

A visual feedback mechanism is needed so users can perceive when the AI Agent is operating SiYuan, while adding a touch of fun.

Choice Made ​

Decoupled file polling architecture:

MCP Server (tool-lifecycle.ts)          Puppy UI (ToolPuppy.svelte)
    │                                        │
    │  writePuppyEvent()                     │  createJsonFilePoller()
    │     ↓                                  │     ↓ every 500ms
    │  puppyEvents.json  ←───────────────────│  POST /api/file/getFile
    │                                        │     ↓
    │                                        │  Parse events → Drive state machine

Key design:

  • Does not directly share JS objects/memory; communicates via filesystem for decoupling
  • Puppy can run independently of the server (test mode)
  • Position persisted via localStorage
  • Animation state machine (idle/reading/writing/deleting/moving/dangerous/success/error)

Rejected Alternatives ​

  • Option A: Directly share JS variables: Too tightly coupled; Puppy component and server must be in the same process.
  • Option B: Use SiYuan broadcast/event bus: Higher complexity, requires native SiYuan support.
  • Option C: WebSocket push: Requires additional ports and connection management; over-engineered.

Current Outcome ​

  • Puppy animations are smooth, state transitions are timely (500ms polling interval)
  • Test mode runs all animations without a backend
  • Fun features like wage card, heart bursts, and feeding increase user engagement

10. Dangerous Action Confirmation Strategy ​

Problem Context ​

Certain operations (delete/remove/find_replace etc.) are destructive and need to prevent accidental AI execution.

Choice Made ​

Protocol confirmation with legacy fallback:

  1. DANGEROUS_ACTIONS set: Hard-coded in config.ts with 15 high-risk actions
  2. Auto-injected warnings: buildAggregatedTool() automatically appends "⚠️ Dangerous action: ... requires user confirmation" to tool descriptions
  3. Server Instructions: server-instructions.ts emphasizes high-risk operations requiring confirmation in MCP instructions
  4. Modern code-level gate: MCP 2026-07-28 calls from clients supporting forms use multi-round elicitation before the tool lifecycle begins; cancellation or rejection returns without executing the action
  5. Compatibility fallback: Legacy MCP clients keep the instruction/help warning flow because they cannot express the new confirmation exchange; CLI command entry remains the user's confirmation
  6. Tool metadata: Conservative annotations expose destructive/read-only hints to capable clients

Rejected Alternatives ​

  • Option A: Require modern elicitation from every client: Rejected because it would break modern and legacy clients without form support. Unsupported clients skip the dialog while retaining permissions and strict preflight checks.
  • Option B: Completely prohibit dangerous actions: Too conservative; many legitimate automation scenarios need delete/remove.
  • Option C: Each dangerous action requires extra token/password: Increases usage barrier, inconsistent with MCP protocol design philosophy.

Current Outcome ​

  • Modern clients receive an enforceable protocol-level confirmation request; legacy clients receive explicit warnings
  • Users can completely disable specific actions via ToolConfig
  • Settings panel provides visual marking for "dangerous actions"

Released under the MIT License.