diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c7ee499 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Node ${{ matrix.node-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node-version: [20, 22] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck and build + run: npm run build + + - name: Run tests + run: npm test + + - name: Check package contents + run: npm pack --dry-run + + - name: Audit production dependencies + run: npm audit --omit=dev --audit-level=high diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..32fefa7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## 0.1.0 - 2026-06-06 + +- Add npm package metadata and CLI binary entrypoint. +- Add Node.js CI for build, tests, packaging, and production dependency audit. +- Add GraphService regression tests for page operations, journals, search, and local file safety. +- Add Codex, Claude Code, and Claude Desktop setup documentation. +- Add contributing and security policy documentation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2925ec2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing + +Thanks for your interest in improving `logseq-mcp`. + +## Development + +Requirements: + +- Node.js 20 or newer +- npm +- A local Logseq graph for manual testing + +Install and build: + +```bash +npm ci +npm run build +npm test +``` + +Run the MCP server locally: + +```bash +LOGSEQ_GRAPH_PATH=/absolute/path/to/logseq/graph npm start +``` + +## Pull Requests + +Before opening a pull request: + +1. Keep changes focused on one behavior or feature. +2. Add or update tests for file access, page operations, or MCP tool changes. +3. Run `npm run build` and `npm test`. +4. Avoid committing local graph data, secrets, or personal notes. + +## Areas Where Help Is Welcome + +- org-mode support +- Logseq property search +- Better graph traversal and filtering +- Cross-platform setup examples +- Security hardening for local file access +- Codex, Claude Code, and Claude Desktop compatibility docs diff --git a/LICENSE b/LICENSE index f2acc30..aaaad46 100644 --- a/LICENSE +++ b/LICENSE @@ -1,135 +1,21 @@ -# Polyform Noncommercial License 1.0.0 - - - -## Acceptance - -In order to get any license under these terms, you must agree -to them as both strict obligations and conditions to all -your licenses. - -## Copyright License - -The licensor grants you a copyright license for the -software to do everything you might do with the software -that would otherwise infringe the licensor's copyright -in it for any permitted purpose. However, you may -only distribute the software according to [Distribution -License](#distribution-license) and make changes or new works -based on the software according to [Changes and New Works -License](#changes-and-new-works-license). - -## Distribution License - -The licensor grants you an additional copyright license -to distribute copies of the software. Your license -to distribute covers distributing the software with -changes and new works permitted by [Changes and New Works -License](#changes-and-new-works-license). - -## Notices - -You must ensure that anyone who gets a copy of any part of -the software from you also gets a copy of these terms or the -URL for them above, as well as copies of any plain-text lines -beginning with `Required Notice:` that the licensor provided -with the software. For example: - -> Required Notice: Copyright dearcloud09 (https://github.com/dearcloud09) - -## Changes and New Works License - -The licensor grants you an additional copyright license to -make changes and new works based on the software for any -permitted purpose. - -## Patent License - -The licensor grants you a patent license for the software that -covers patent claims the licensor can license, or becomes able -to license, that you would infringe by using the software. - -## Noncommercial Purposes - -Any noncommercial purpose is a permitted purpose. - -## Personal Uses - -Personal use for research, experiment, and testing for -the benefit of public knowledge, personal study, private -entertainment, hobby projects, amateur pursuits, or religious -observance, without any anticipated commercial application, -is use for a permitted purpose. - -## Noncommercial Organizations - -Use by any charitable organization, educational institution, -public research organization, public safety or health -organization, environmental protection organization, -or government institution is use for a permitted purpose -regardless of the source of funding or obligations resulting -from the funding. - -## Fair Use - -You may have "fair use" rights for the software under the -law. These terms do not limit them. - -## No Other Rights - -These terms do not allow you to sublicense or transfer any of -your licenses to anyone else, or prevent the licensor from -granting licenses to anyone else. These terms do not imply -any other licenses. - -## Patent Defense - -If you make any written claim that the software infringes or -contributes to infringement of any patent, your patent license -for the software granted under these terms ends immediately. If -your company makes such a claim, your patent license ends -immediately for work on behalf of your company. - -## Violations - -The first time you are notified in writing that you have -violated any of these terms, or done anything with the software -not covered by your licenses, your licenses can nonetheless -continue if you come into full compliance with these terms, -and take practical steps to correct past violations, within -32 days of receiving notice. Otherwise, all your licenses -end immediately. - -## No Liability - -***As far as the law allows, the software comes as is, without -any warranty or condition, and the licensor will not be liable -to you for any damages arising out of these terms or the use -or nature of the software, under any kind of legal claim.*** - -## Definitions - -The **licensor** is the individual or entity offering these -terms, and the **software** is the software the licensor makes -available under these terms. - -**You** refers to the individual or entity agreeing to these -terms. - -**Your company** is any legal entity, sole proprietorship, -or other kind of organization that you work for, plus all -organizations that have control over, are under the control of, -or are under common control with that organization. **Control** -means ownership of substantially all the assets of an entity, -or the power to direct its management and policies by vote, -contract, or otherwise. Control can be direct or indirect. - -**Your licenses** are all the licenses granted to you for the -software under these terms. - -**Use** means anything you do with the software requiring one -of your licenses. - ---- - -Required Notice: Copyright 2026 dearcloud09 (https://github.com/dearcloud09) +MIT License + +Copyright (c) 2026 dearcloud09 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.ko.md b/README.ko.md index 01773b5..4eecea7 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,14 +1,14 @@ # Logseq MCP Server -[![License: Polyform Noncommercial](https://img.shields.io/badge/License-Polyform%20NC-red.svg)](https://polyformproject.org/licenses/noncommercial/1.0.0) -[![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)](https://nodejs.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Node.js](https://img.shields.io/badge/Node.js-20%2B-green.svg)](https://nodejs.org/) [![MCP](https://img.shields.io/badge/MCP-Compatible-purple.svg)](https://modelcontextprotocol.io/) > **AI가 당신의 Logseq 그래프를 직접 읽고 쓸 수 있게 해주는 MCP 서버** [English README](README.md) -Claude와 대화하면서 "오늘 저널에 이거 추가해줘", "지난주에 뭐했는지 찾아봐", "이 페이지랑 연결된 거 다 보여줘"가 가능해집니다. +Codex, Claude Code, Claude Desktop과 대화하면서 "오늘 저널에 이거 추가해줘", "지난주에 뭐했는지 찾아봐", "이 페이지랑 연결된 거 다 보여줘"가 가능해집니다. --- @@ -17,7 +17,7 @@ Claude와 대화하면서 "오늘 저널에 이거 추가해줘", "지난주에 **문제**: Logseq는 훌륭한 PKM 도구지만, AI 어시스턴트와 연동하려면 매번 복사-붙여넣기가 필요합니다. **해결**: 이 MCP 서버를 사용하면: -- Claude가 **직접** 저널에 기록 (복사-붙여넣기 불필요) +- AI 어시스턴트가 **직접** 저널에 기록 (복사-붙여넣기 불필요) - 과거 기록을 **검색하고 요약** (맥락 유지) - 페이지 간 **연결 관계 탐색** (백링크, 그래프) - 템플릿 기반 **저널 자동 생성** @@ -35,7 +35,7 @@ Claude: [logseq-mcp로 직접 저널에 기록] ### Good fit if you... - Logseq를 **주력 PKM**으로 사용 중 -- Claude Code나 Claude Desktop을 **일상적으로 사용** +- Codex, Claude Code, Claude Desktop을 **일상적으로 사용** - AI에게 노트 관리를 **위임**하고 싶음 - **로컬 파일 기반** Logseq 사용 (Logseq Sync 아님) @@ -82,14 +82,38 @@ Claude: [get_backlinks 실행] ### 1. 설치 +배포 패키지 사용: + +```bash +npx -y logseq-mcp +``` + +개발용 로컬 체크아웃 사용: + ```bash git clone https://github.com/dearcloud09/logseq-mcp.git cd logseq-mcp -npm install +npm ci npm run build ``` -### 2. 설정 +### 2. Codex 설정 + +`~/.codex/config.toml`에 MCP 서버를 추가합니다: + +```toml +[mcp_servers.logseq] +command = "npx" +args = ["-y", "logseq-mcp"] + +[mcp_servers.logseq.env] +LOGSEQ_GRAPH_PATH = "/path/to/your/logseq/graph" +WEATHER_LOCATION = "서울" +``` + +Codex에서 `/mcp`를 실행해 `logseq` 서버가 연결되었는지 확인합니다. + +### 3. Claude 설정 **Claude Code** (`~/.claude/settings.json`): @@ -97,8 +121,8 @@ npm run build { "mcpServers": { "logseq": { - "command": "node", - "args": ["/path/to/logseq-mcp/dist/index.js"], + "command": "npx", + "args": ["-y", "logseq-mcp"], "env": { "LOGSEQ_GRAPH_PATH": "/path/to/your/logseq/graph", "WEATHER_LOCATION": "서울" @@ -116,8 +140,8 @@ npm run build { "mcpServers": { "logseq": { - "command": "node", - "args": ["/path/to/logseq-mcp/dist/index.js"], + "command": "npx", + "args": ["-y", "logseq-mcp"], "env": { "LOGSEQ_GRAPH_PATH": "/path/to/your/logseq/graph", "WEATHER_LOCATION": "서울" @@ -127,9 +151,12 @@ npm run build } ``` -### 3. 확인 +로컬 체크아웃을 직접 실행하려면 command를 `node`, args를 +`["/absolute/path/to/logseq-mcp/dist/index.js"]`로 바꿉니다. + +### 4. 확인 -Claude에게 물어보세요: "내 Logseq 페이지 목록 보여줘" +MCP 클라이언트에게 물어보세요: "내 Logseq 페이지 목록 보여줘" --- @@ -183,7 +210,9 @@ your-graph/ ## Security -Graph 외부 파일 접근 차단, 입력 검증, DoS 방지 등 보안 강화 적용됨. +Graph 외부 파일 접근 차단, 심링크/하드링크 차단, 입력 검증, DoS 방지, +에러 메시지 정제 등 보안 강화가 적용되어 있습니다. 자세한 보안 모델은 +[SECURITY.md](SECURITY.md)를 참고하세요. --- @@ -193,11 +222,12 @@ Graph 외부 파일 접근 차단, 입력 검증, DoS 방지 등 보안 강화 `LOGSEQ_GRAPH_PATH` 환경변수가 설정되지 않았습니다. 설정 파일에서 경로를 확인하세요. -### MCP 서버가 Claude에서 인식되지 않음 +### MCP 서버가 Codex 또는 Claude에서 인식되지 않음 -1. Claude Code/Desktop 재시작 -2. 경로가 절대 경로인지 확인 (`/Users/...` 형식) -3. `npm run build` 실행 확인 +1. Node.js 20 이상이 설치되어 있는지 확인 +2. `LOGSEQ_GRAPH_PATH`가 절대 경로인지 확인 (`/Users/...` 형식) +3. Codex에서는 `/mcp`를 실행해 `logseq` 서버가 보이는지 확인 +4. 로컬 체크아웃을 사용하는 경우 `npm run build` 실행 확인 ### 페이지가 보이지 않음 @@ -292,12 +322,21 @@ Logseq 쿼리 예시: ## Development ```bash +# 의존성 설치 +npm ci + # 개발 모드 (hot reload) npm run dev # TypeScript 빌드 npm run build +# 회귀 테스트 +npm test + +# npm 패키지 내용 확인 +npm pack --dry-run + # 프로덕션 실행 npm start ``` @@ -315,7 +354,7 @@ src/ ## Contributing -이슈와 PR 환영합니다! +이슈와 PR 환영합니다! 자세한 개발/테스트 가이드는 [CONTRIBUTING.md](CONTRIBUTING.md)를 참고하세요. 1. Fork this repo 2. Create feature branch (`git checkout -b feature/amazing`) @@ -334,7 +373,7 @@ src/ ## License -[Polyform Noncommercial 1.0.0](LICENSE) - 개인 및 비상업적 사용 무료. +[MIT](LICENSE) --- @@ -342,4 +381,5 @@ src/ - [Model Context Protocol](https://modelcontextprotocol.io/) - [Logseq](https://logseq.com/) +- [Codex](https://developers.openai.com/codex/) - [Claude Code](https://claude.com/claude-code) diff --git a/README.md b/README.md index fde782e..2d76f5a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # Logseq MCP Server -[![License: Polyform Noncommercial](https://img.shields.io/badge/License-Polyform%20NC-red.svg)](https://polyformproject.org/licenses/noncommercial/1.0.0) -[![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)](https://nodejs.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Node.js](https://img.shields.io/badge/Node.js-20%2B-green.svg)](https://nodejs.org/) [![MCP](https://img.shields.io/badge/MCP-Compatible-purple.svg)](https://modelcontextprotocol.io/) > **Let AI read and write your Logseq graph directly via MCP** [한국어 README](README.ko.md) -Talk to Claude and say "add this to today's journal", "find what I did last week", "show me all pages linked to this one" - and it just works. +Ask Codex, Claude Code, or Claude Desktop to "add this to today's journal", "find what I did last week", or "show me all pages linked to this one" - and it just works. --- @@ -35,7 +35,7 @@ Claude: [writes directly to Logseq via logseq-mcp] ### Good fit if you... - Use Logseq as your **primary PKM** -- Use **Claude Code or Claude Desktop** regularly +- Use **Codex, Claude Code, or Claude Desktop** regularly - Want to **delegate** note management to AI - Use **local file-based** Logseq (not Logseq Sync) @@ -65,14 +65,37 @@ Claude: [writes directly to Logseq via logseq-mcp] ### 1. Install +Use the published package: + +```bash +npx -y logseq-mcp +``` + +Or install from a local checkout for development: + ```bash git clone https://github.com/dearcloud09/logseq-mcp.git cd logseq-mcp -npm install +npm ci npm run build ``` -### 2. Configure +### 2. Configure Codex + +Add the MCP server to `~/.codex/config.toml`: + +```toml +[mcp_servers.logseq] +command = "npx" +args = ["-y", "logseq-mcp"] + +[mcp_servers.logseq.env] +LOGSEQ_GRAPH_PATH = "/path/to/your/logseq/graph" +``` + +Then open Codex and run `/mcp` to confirm the `logseq` server is connected. + +### 3. Configure Claude **Claude Code** (`~/.claude/settings.json`): @@ -80,8 +103,8 @@ npm run build { "mcpServers": { "logseq": { - "command": "node", - "args": ["/path/to/logseq-mcp/dist/index.js"], + "command": "npx", + "args": ["-y", "logseq-mcp"], "env": { "LOGSEQ_GRAPH_PATH": "/path/to/your/logseq/graph" } @@ -96,8 +119,8 @@ npm run build { "mcpServers": { "logseq": { - "command": "node", - "args": ["/path/to/logseq-mcp/dist/index.js"], + "command": "npx", + "args": ["-y", "logseq-mcp"], "env": { "LOGSEQ_GRAPH_PATH": "/path/to/your/logseq/graph" } @@ -106,9 +129,12 @@ npm run build } ``` -### 3. Verify +For a local checkout, replace the command with `node` and the args with +`["/absolute/path/to/logseq-mcp/dist/index.js"]`. -Ask Claude: "Show me my Logseq page list" +### 4. Verify + +Ask your MCP client: "Show me my Logseq page list" --- @@ -168,6 +194,8 @@ your-graph/ - DoS protection (content size limits) - Error message sanitization +See [SECURITY.md](SECURITY.md) for the full security model. + --- ## Troubleshooting @@ -176,11 +204,12 @@ your-graph/ Set `LOGSEQ_GRAPH_PATH` in your configuration file. -### MCP server not recognized by Claude +### MCP server not recognized by Codex or Claude -1. Restart Claude Code/Desktop -2. Verify path is absolute (`/Users/...` format) -3. Ensure `npm run build` was executed +1. Confirm Node.js 20+ is installed +2. Confirm `LOGSEQ_GRAPH_PATH` is an absolute path (`/Users/...` format) +3. In Codex, run `/mcp` and check whether `logseq` is listed +4. For a local checkout, ensure `npm run build` was executed ### Pages not showing up @@ -248,12 +277,21 @@ See [Korean README](README.ko.md) for more details. ## Development ```bash +# Install dependencies +npm ci + # Development mode (watch) npm run dev # TypeScript build npm run build +# Run regression tests +npm test + +# Check npm package contents +npm pack --dry-run + # Production run npm start ``` @@ -271,13 +309,8 @@ src/ ## Contributing -Issues and PRs welcome! - -1. Fork this repo -2. Create feature branch (`git checkout -b feature/amazing`) -3. Commit changes (`git commit -m 'Add amazing feature'`) -4. Push to branch (`git push origin feature/amazing`) -5. Open a Pull Request +Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) +for setup, testing, and contribution guidelines. ### Ideas for contribution @@ -291,7 +324,7 @@ Issues and PRs welcome! ## License -[Polyform Noncommercial 1.0.0](LICENSE) - Free for personal and noncommercial use. +[MIT](LICENSE) --- @@ -299,4 +332,5 @@ Issues and PRs welcome! - [Model Context Protocol](https://modelcontextprotocol.io/) - [Logseq](https://logseq.com/) +- [Codex](https://developers.openai.com/codex/) - [Claude Code](https://claude.ai/code) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d632a80 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,42 @@ +# Security Policy + +`logseq-mcp` gives AI assistants read and write access to a local Logseq graph. +Treat the configured graph path as sensitive. + +## Supported Versions + +Security fixes are currently provided for the latest release. + +## Reporting a Vulnerability + +Please do not open a public issue for vulnerabilities that could expose private +notes, local files, or credentials. + +Report issues by emailing the maintainer or opening a private GitHub security +advisory if that option is available. Include: + +- affected version or commit +- operating system +- configured `LOGSEQ_GRAPH_PATH` +- reproduction steps using a minimal test graph +- expected and actual behavior + +## Security Model + +The server is intended to: + +- access only the configured Logseq graph +- read and write only Markdown pages in `pages/` and `journals/` +- reject path traversal outside the graph +- reject symbolic links and hardlinks +- limit input and content sizes +- sanitize filesystem error messages before returning them to MCP clients + +The server does not: + +- authenticate MCP clients +- encrypt local notes +- protect against an already-compromised MCP client +- support remote multi-user hosting + +Run it only with MCP clients you trust. diff --git a/package-lock.json b/package-lock.json index f91d033..fb69aff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,35 @@ { "name": "logseq-mcp", - "version": "1.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "logseq-mcp", - "version": "1.0.0", - "license": "ISC", + "version": "0.1.0", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", - "axios": "^1.13.2", - "cheerio": "^1.1.2", + "@modelcontextprotocol/sdk": "^1.29.0", + "axios": "^1.17.0", + "cheerio": "^1.2.0", "zod": "^3.23.8" }, + "bin": { + "logseq-mcp": "dist/index.js" + }, "devDependencies": { "@types/cheerio": "^0.22.35", "@types/node": "^20.10.0", "typescript": "^5.3.0" + }, + "engines": { + "node": ">=20" } }, "node_modules/@hono/node-server": { - "version": "1.19.7", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", - "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -33,12 +39,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", - "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -46,14 +52,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -104,10 +111,22 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -144,20 +163,21 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -166,7 +186,7 @@ "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", + "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" }, @@ -223,9 +243,9 @@ } }, "node_modules/cheerio": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", - "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", @@ -233,11 +253,11 @@ "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.0.0", + "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^7.12.0", + "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" }, "engines": { @@ -277,9 +297,9 @@ } }, "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "engines": { "node": ">=18" @@ -652,10 +672,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, "engines": { "node": ">= 16" }, @@ -673,9 +696,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -710,9 +733,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -882,19 +905,18 @@ } }, "node_modules/hono": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz", - "integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==", + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } }, "node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -906,14 +928,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -942,10 +964,23 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", - "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -964,6 +999,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1198,9 +1242,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -1230,15 +1274,18 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -1397,13 +1444,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -1468,17 +1515,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typescript": { @@ -1496,9 +1560,9 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.1.tgz", + "integrity": "sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg==", "license": "MIT", "engines": { "node": ">=20.18.1" diff --git a/package.json b/package.json index 8b3f81d..7af0384 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,58 @@ { "name": "logseq-mcp", - "version": "1.0.0", + "version": "0.1.0", "description": "MCP server for Logseq graph integration", "type": "module", "main": "dist/index.js", + "bin": { + "logseq-mcp": "./dist/index.js" + }, + "files": [ + "dist", + "README.md", + "README.ko.md", + "CHANGELOG.md", + "CONTRIBUTING.md", + "SECURITY.md", + "LICENSE" + ], "scripts": { "build": "tsc", "start": "node dist/index.js", - "dev": "tsc --watch" + "dev": "tsc --watch", + "test": "npm run build && node --test test/*.test.mjs", + "typecheck": "tsc --noEmit", + "prepack": "npm run build" }, "keywords": [ "mcp", "logseq", - "ai" + "ai", + "codex", + "claude", + "pkm", + "knowledge-graph" ], - "license": "SEE LICENSE IN LICENSE", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/dearcloud09/logseq-mcp.git" + }, + "bugs": { + "url": "https://github.com/dearcloud09/logseq-mcp/issues" + }, + "homepage": "https://github.com/dearcloud09/logseq-mcp#readme", + "engines": { + "node": ">=20" + }, + "overrides": { + "ajv": "^8.20.0", + "fast-uri": "^3.1.2" + }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", - "axios": "^1.13.2", - "cheerio": "^1.1.2", + "@modelcontextprotocol/sdk": "^1.29.0", + "axios": "^1.17.0", + "cheerio": "^1.2.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/src/graph.ts b/src/graph.ts index 3546f5e..82db0df 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -4,7 +4,7 @@ */ import { readdir, readFile, writeFile, unlink, mkdir, stat, lstat } from 'node:fs/promises'; -import { join, basename, extname, resolve } from 'node:path'; +import { join, basename, extname, resolve, dirname } from 'node:path'; import type { Page, PageMetadata, SearchResult, SearchMatch, Graph, GraphNode, GraphEdge } from './types.js'; // 보안 상수 @@ -17,9 +17,9 @@ export class GraphService { private journalsPath: string; constructor(graphPath: string) { - this.graphPath = graphPath; - this.pagesPath = join(graphPath, 'pages'); - this.journalsPath = join(graphPath, 'journals'); + this.graphPath = resolve(graphPath); + this.pagesPath = join(this.graphPath, 'pages'); + this.journalsPath = join(this.graphPath, 'journals'); } /** @@ -78,6 +78,11 @@ export class GraphService { } catch { continue; } + try { + await this.checkRegularFile(filePath); + } catch { + continue; + } const content = await readFile(filePath, 'utf-8'); const name = basename(file, '.md'); pages.push({ @@ -107,6 +112,11 @@ export class GraphService { } catch { continue; } + try { + await this.checkRegularFile(filePath); + } catch { + continue; + } const content = await readFile(filePath, 'utf-8'); const name = basename(file, '.md'); pages.push({ @@ -131,7 +141,7 @@ export class GraphService { */ async readPage(pathOrName: string): Promise { const filePath = await this.resolvePath(pathOrName); - await this.checkSymlink(filePath); // 심링크 공격 방지 + await this.checkRegularFile(filePath); // 심링크/하드링크 공격 방지 const content = await readFile(filePath, 'utf-8'); const name = basename(filePath, '.md'); const isJournal = filePath.includes('/journals/'); @@ -231,7 +241,7 @@ export class GraphService { this.validateContentSize(content); const filePath = await this.resolvePath(pathOrName); - await this.checkSymlink(filePath); // 심링크 공격 방지 + await this.checkRegularFile(filePath); // 심링크/하드링크 공격 방지 const fullContent = this.buildContent(content, properties); await writeFile(filePath, fullContent, 'utf-8'); return this.readPage(pathOrName); @@ -242,7 +252,7 @@ export class GraphService { */ async deletePage(pathOrName: string): Promise { const filePath = await this.resolvePath(pathOrName); - await this.checkSymlink(filePath); // 보안: 심링크 공격 방지 + await this.checkRegularFile(filePath); // 보안: 심링크/하드링크 공격 방지 await unlink(filePath); } @@ -254,7 +264,7 @@ export class GraphService { this.validateContentSize(content); const filePath = await this.resolvePath(pathOrName); - await this.checkSymlink(filePath); // 심링크 공격 방지 + await this.checkRegularFile(filePath); // 심링크/하드링크 공격 방지 const existing = await readFile(filePath, 'utf-8'); const newContent = existing.trimEnd() + '\n' + content; @@ -567,7 +577,7 @@ export class GraphService { */ private validatePath(filePath: string): string { const normalizedPath = resolve(filePath); - const normalizedGraphPath = resolve(this.graphPath); + const normalizedGraphPath = this.graphPath; if (!normalizedPath.startsWith(normalizedGraphPath + '/') && normalizedPath !== normalizedGraphPath) { throw new Error(`Access denied: path outside graph directory`); @@ -576,11 +586,29 @@ export class GraphService { return normalizedPath; } + private validateGraphPagePath(filePath: string): string { + const normalizedPath = this.validatePath(filePath); + const normalizedDir = dirname(normalizedPath); + + if (normalizedDir !== this.pagesPath && normalizedDir !== this.journalsPath) { + throw new Error(`Access denied: only pages and journals are allowed`); + } + + if (extname(normalizedPath) !== '.md') { + throw new Error(`Access denied: only markdown pages are allowed`); + } + + return normalizedPath; + } + private async resolvePath(pathOrName: string): Promise { // If it's already a relative path if (pathOrName.includes('/')) { - const filePath = join(this.graphPath, pathOrName); - return this.validatePath(filePath); + const withExtension = extname(pathOrName) ? pathOrName : `${pathOrName}.md`; + const filePath = join(this.graphPath, withExtension); + const validPath = this.validateGraphPagePath(filePath); + await stat(validPath); + return validPath; } // Try pages first @@ -654,6 +682,10 @@ export class GraphService { } } + while (bodyStart < lines.length && lines[bodyStart].trim() === '') { + bodyStart++; + } + return { properties, body: lines.slice(bodyStart).join('\n'), diff --git a/test/graph-service.test.mjs b/test/graph-service.test.mjs new file mode 100644 index 0000000..5413b8d --- /dev/null +++ b/test/graph-service.test.mjs @@ -0,0 +1,128 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, mkdir, writeFile, symlink, link, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import { GraphService } from '../dist/graph.js'; + +async function withGraph(fn) { + const root = await mkdtemp(join(tmpdir(), 'logseq-mcp-test-')); + await mkdir(join(root, 'pages'), { recursive: true }); + await mkdir(join(root, 'journals'), { recursive: true }); + + try { + return await fn(root, new GraphService(root)); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +test('creates, reads, appends, and searches markdown pages', async () => { + await withGraph(async (_root, graph) => { + const created = await graph.createPage( + 'Project A', + '- Sprint notes #meeting\n- Related to [[Goals]]', + { status: 'active' } + ); + + assert.equal(created.name, 'Project A'); + assert.equal(created.path, 'pages/Project A.md'); + assert.deepEqual(created.properties, { status: 'active' }); + assert.deepEqual(created.tags, ['meeting']); + assert.deepEqual(created.links, ['Goals']); + + const readByPath = await graph.readPage('pages/Project A'); + assert.equal(readByPath.content, '- Sprint notes #meeting\n- Related to [[Goals]]'); + + const appended = await graph.appendToPage('Project A', '- Follow-up item'); + assert.match(appended.content, /Follow-up item/); + + const results = await graph.searchPages('Sprint', { tags: ['meeting'], folder: 'pages' }); + assert.equal(results.length, 1); + assert.equal(results[0].page.name, 'Project A'); + assert.equal(results[0].matches[0].line, 3); + }); +}); + +test('creates and reads dated journal pages', async () => { + await withGraph(async (_root, graph) => { + const journal = await graph.createJournalPage('2026-06-06', '- Daily note'); + + assert.equal(journal.name, '2026_06_06'); + assert.equal(journal.path, 'journals/2026_06_06.md'); + assert.equal(journal.isJournal, true); + + const read = await graph.getJournalPage('2026-06-06'); + assert.equal(read?.content, '- Daily note'); + + await assert.rejects( + () => graph.getJournalPage('20260606'), + /Invalid date format/ + ); + }); +}); + +test('blocks traversal and non-page graph files', async () => { + await withGraph(async (root, graph) => { + await mkdir(join(root, 'logseq'), { recursive: true }); + await writeFile(join(root, 'logseq', 'config.edn'), '{:secret true}', 'utf-8'); + + await assert.rejects( + () => graph.readPage('../outside'), + /Access denied/ + ); + + await assert.rejects( + () => graph.readPage('logseq/config.edn'), + /Access denied/ + ); + }); +}); + +test('skips symbolic links and hardlinks when listing or reading pages', async (t) => { + await withGraph(async (root, graph) => { + await writeFile(join(root, 'pages', 'Normal.md'), '- Safe page', 'utf-8'); + + const outside = join(root, '..', 'outside-logseq-mcp-test.md'); + await writeFile(outside, '- Outside file', 'utf-8'); + t.after(async () => { + await rm(outside, { force: true }); + }); + + await symlink(outside, join(root, 'pages', 'Linked.md')); + await link(outside, join(root, 'pages', 'Hardlinked.md')); + + const pages = await graph.listPages('pages'); + assert.deepEqual(pages.map((page) => page.name), ['Normal']); + + await assert.rejects( + () => graph.readPage('Linked'), + /symbolic links/ + ); + + await assert.rejects( + () => graph.readPage('Hardlinked'), + /hardlinks/ + ); + }); +}); + +test('validates unsafe names, oversized content, and property injection', async () => { + await withGraph(async (_root, graph) => { + await assert.rejects( + () => graph.createPage('../bad', '- bad'), + /Invalid page name/ + ); + + await assert.rejects( + () => graph.createPage('Big Page', 'x'.repeat(10 * 1024 * 1024 + 1)), + /Content too large/ + ); + + await assert.rejects( + () => graph.createPage('Bad Properties', '- body', { 'bad\nkey': 'value' }), + /Invalid property/ + ); + }); +});