Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,37 @@ AnythingLLM.
sources, and turn a notebook into a mind map.
- **No deployment.** Download, open, start reading. The embedder downloads once
and then runs on your machine.
- **An MCP export surface.** Serve a notebook to a local agent over read-only
stdio [MCP](https://modelcontextprotocol.io) — the same retrieval path the app
uses, as a server, not a second client (see below).

## Use it from an agent (MCP)

KnowNote can serve your library over MCP on stdio, read-only, so an agent —
Claude Code, Codex, Cursor, Claude Desktop — can search the documents you already
have instead of asking you to paste them in.

```jsonc
// your MCP client's config
{
"mcpServers": {
"knownote": {
"command": "/path/to/knownote",
"args": ["--mcp"]
}
}
}
```

The tools are `list_notebooks`, `search_notebook`, `get_source`, `read_document`
and `search_notes`. `search_notebook` returns passages **with provenance**
(document id, page, character offsets) rather than bare text, so a claim an agent
makes can be checked against the source. There is no write tool, no tool that
calls a model, and no result that reports a filesystem path — that is what makes
this surface safe to grant to an agent reading untrusted documents.

By default the server uses the same library as the desktop app. Point it at a
different profile with `KNOWNOTE_DATA_DIR=/path/to/profile`.

## What is not here yet

Expand Down
25 changes: 25 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,31 @@ KnowNote 把你自己的文档变成可以提问的知识库,并让每个回
- **自带模型。** 任何兼容 OpenAI、Anthropic 或 Google 协议的端点,或 Ollama 这类本地服务。没有内置对话模型,也没有账号。
- **笔记与思维导图。** 把你的结论作为结构化笔记保存在资料旁边,也可以把一本笔记本转成思维导图。
- **无需部署。** 下载、打开、开始阅读。嵌入模型只下载一次,之后都在你的机器上运行。
- **MCP 导出面。** 通过只读的 stdio [MCP](https://modelcontextprotocol.io) 把笔记本提供给本地 agent —— 用的是 app 自己的检索路径,KnowNote 作服务端,不是再做一个客户端(见下)。

## 从 agent 调用(MCP)

KnowNote 可以通过 stdio 以只读方式把知识库作为 MCP 服务提供给 agent ——
Claude Code、Codex、Cursor、Claude Desktop —— 让它直接检索你已有的文档,而不是让你把文档粘进去。

```jsonc
// 你的 MCP 客户端配置
{
"mcpServers": {
"knownote": {
"command": "/path/to/knownote",
"args": ["--mcp"]
}
}
}
```

工具是 `list_notebooks`、`search_notebook`、`get_source`、`read_document`、`search_notes`。
`search_notebook` 返回带 **provenance**(文档 id、页码、字符偏移)的片段,而不是裸文本,
所以 agent 给出的结论可以回到原文核对。没有写入工具、没有调用模型的工具、也没有任何
返回文件路径的结果 —— 正是这一点让这个面可以安全地授予一个正在读不可信文档的 agent。

默认使用与桌面应用相同的知识库;用 `KNOWNOTE_DATA_DIR=/path/to/profile` 可以指向别的 profile。

## 还没有的部分

Expand Down
80 changes: 79 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"dependencies": {
"@huggingface/transformers": "^4.3.0",
"@mixmark-io/domino": "^2.2.0",
"@modelcontextprotocol/server": "^2.1.0",
"anki-apkg-export": "^4.0.0",
"better-sqlite3": "^12.6.2",
"jsdom": "^27.4.0",
Expand Down Expand Up @@ -129,6 +130,7 @@
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/tsconfig": "^2.0.0",
"@electron-toolkit/utils": "^4.0.0",
"@modelcontextprotocol/client": "^2.1.0",
"@mozilla/readability": "^0.6.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
Expand Down
13 changes: 11 additions & 2 deletions src/main/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { app } from 'electron'
import { join } from 'path'
import { mkdirSync } from 'fs'
import { stat } from 'fs/promises'
import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
Expand All @@ -23,8 +24,16 @@ let isClosing = false // 防止重复关闭
* 在 Electron 主进程的 app.whenReady() 中调用
*/
export function initDatabase() {
// 数据库文件存放在用户数据目录
const dbPath = join(app.getPath('userData'), 'knownote.db')
// 数据库文件存放在用户数据目录。
//
// `KNOWNOTE_DATA_DIR` 让非桌面入口(#80 的 MCP server)能指向同一个 profile,而不
// 必经过 Electron 的 `app.getPath('userData')`。默认值仍然是 Electron 的 userData,
// 所以桌面应用的行为不变。
const dataDir = process.env.KNOWNOTE_DATA_DIR?.trim() || app.getPath('userData')
// Electron creates its userData directory; a `KNOWNOTE_DATA_DIR` override (#80) is
// a path we were handed, so it has to exist before sqlite can open a file in it.
mkdirSync(dataDir, { recursive: true })
const dbPath = join(dataDir, 'knownote.db')

console.log('[Database] Initializing database at:', dbPath)

Expand Down
9 changes: 9 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { getStore } from './config/store'
import { migrateProvidersToConnections } from './config/connectionMigration'
import { isSmokeTestRequested, runSmokeTest } from './smokeTest'
import { isEvalRequested, runEvalCli } from './eval/run'
import { isMcpRequested, runMcpServer } from './mcp/entry'
import { declareDocumentScheme, registerDocumentProtocolHandler } from './protocol/documentProtocol'
import {
declarePdfjsAssetScheme,
Expand Down Expand Up @@ -70,6 +71,14 @@ app.whenReady().then(async () => {
return
}

// MCP stdio server (#80). Like the other headless entries it runs before any
// window exists; unlike them it stays alive until the client closes stdin.
if (isMcpRequested()) {
await runMcpServer()
app.exit(0)
return
}

// Set app user model id for windows
electronApp.setAppUserModelId('com.knownote.app')

Expand Down
61 changes: 61 additions & 0 deletions src/main/mcp/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { join } from 'path'
import { app } from 'electron'
import { serveStdio } from '@modelcontextprotocol/server/stdio'
import { closeDatabase, initDatabase, initVectorStore, runMigrations } from '../db'
import { ConnectionManager } from '../models/ConnectionManager'
import { EmbeddingService } from '../services/EmbeddingService'
import { KnowledgeService } from '../services/KnowledgeService'
import { createKnowledgeRuntime } from './runtime'
import { buildMcpServer } from './server'

export const MCP_FLAG = '--mcp'

export function isMcpRequested(argv: readonly string[] = process.argv): boolean {
return argv.includes(MCP_FLAG)
}

/**
* Serve the knowledge base over MCP on stdio (#80).
*
* The stdio transport **is** the protocol: the JSON-RPC frames and the process's
* standard output are the same bytes. Anything the app logs to stdout would be
* read by the client as a frame, and the database logs on init — so every console
* method is redirected to stderr *before* anything else runs. This patches the
* console methods, not `process.stdout`: the SDK writes to that stream directly.
*
* The runtime is built once here, outside the factory. `serveStdio` may call the
* factory more than once for a single connection (an optimistic modern probe,
* then a legacy instance when the probe falls back), so opening the database or
* the index inside the factory would do it twice.
*
* Resolves when the client closes stdin, which is how a stdio server is supposed
* to end.
*/
export async function runMcpServer(): Promise<void> {
console.log = console.error
console.info = console.error
console.debug = console.error

initDatabase()
runMigrations()
initVectorStore()

const connectionManager = new ConnectionManager()
const embeddingService = new EmbeddingService(connectionManager, {
cacheDir: join(app.getPath('userData'), 'models')
})
const knowledgeService = new KnowledgeService(embeddingService)
const runtime = createKnowledgeRuntime(knowledgeService)

// Dual-era by default: `legacy` is left as the SDK's 'serve', so a 2025-era
// `initialize` and a 2026-07-28 per-request envelope are both served by the same
// factory — one tool surface, no second implementation.
serveStdio(() => buildMcpServer(runtime, app.getVersion()))

await new Promise<void>((resolve) => {
process.stdin.once('end', resolve)
process.stdin.once('close', resolve)
})

closeDatabase()
}
Loading
Loading