From e73b9a2f9ace688c6a38ccf50ff6e34c8d1d000d Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 24 Jan 2026 13:15:29 +0900 Subject: [PATCH 01/40] Enable Dependabot Follow-up to https://github.com/modelcontextprotocol/servers/pull/3233. Configure Dependabot to automatically detect updates to the actions used in GitHub Actions (e.g., `actions/checkout`) and create pull requests for them. Based on experience, checking for updates on a weekly interval should work well. --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b18fd29 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' From 9a96ea6e5913736f92b88345bf51caeaaa8e719f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 10 Jul 2026 03:30:18 +0000 Subject: [PATCH 02/40] Automatic update of packages --- src/fetch/pyproject.toml | 2 +- src/fetch/uv.lock | 2 +- src/filesystem/package.json | 4 ++-- src/git/pyproject.toml | 2 +- src/git/uv.lock | 2 +- src/time/pyproject.toml | 2 +- src/time/uv.lock | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/fetch/pyproject.toml b/src/fetch/pyproject.toml index 84735f2..4b001d7 100644 --- a/src/fetch/pyproject.toml +++ b/src/fetch/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-server-fetch" -version = "0.6.3" +version = "2026.7.10" description = "A Model Context Protocol server providing tools to fetch and convert web content for usage by LLMs" readme = "README.md" requires-python = ">=3.10" diff --git a/src/fetch/uv.lock b/src/fetch/uv.lock index ea9b056..83b28be 100644 --- a/src/fetch/uv.lock +++ b/src/fetch/uv.lock @@ -558,7 +558,7 @@ wheels = [ [[package]] name = "mcp-server-fetch" -version = "0.6.3" +version = "2026.7.10" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/src/filesystem/package.json b/src/filesystem/package.json index c943ce2..5ab7563 100644 --- a/src/filesystem/package.json +++ b/src/filesystem/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-filesystem", - "version": "0.6.3", + "version": "2026.7.10", "description": "MCP server for filesystem access", "license": "SEE LICENSE IN LICENSE", "mcpName": "io.github.modelcontextprotocol/server-filesystem", @@ -39,4 +39,4 @@ "typescript": "^5.8.2", "vitest": "^4.1.8" } -} +} \ No newline at end of file diff --git a/src/git/pyproject.toml b/src/git/pyproject.toml index c5113ae..3925021 100644 --- a/src/git/pyproject.toml +++ b/src/git/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-server-git" -version = "0.6.2" +version = "2026.7.10" description = "A Model Context Protocol server providing tools to read, search, and manipulate Git repositories programmatically via LLMs" readme = "README.md" requires-python = ">=3.10" diff --git a/src/git/uv.lock b/src/git/uv.lock index a838e3d..dda3f82 100644 --- a/src/git/uv.lock +++ b/src/git/uv.lock @@ -356,7 +356,7 @@ wheels = [ [[package]] name = "mcp-server-git" -version = "0.6.2" +version = "2026.7.10" source = { editable = "." } dependencies = [ { name = "click" }, diff --git a/src/time/pyproject.toml b/src/time/pyproject.toml index f9be050..9327f66 100644 --- a/src/time/pyproject.toml +++ b/src/time/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-server-time" -version = "0.6.2" +version = "2026.7.10" description = "A Model Context Protocol server providing tools for time queries and timezone conversions for LLMs" readme = "README.md" requires-python = ">=3.10" diff --git a/src/time/uv.lock b/src/time/uv.lock index e77b3c3..dca8429 100644 --- a/src/time/uv.lock +++ b/src/time/uv.lock @@ -343,7 +343,7 @@ wheels = [ [[package]] name = "mcp-server-time" -version = "0.6.2" +version = "2026.7.10" source = { editable = "." } dependencies = [ { name = "mcp" }, From 63df72cb15b14ba68d8eaedbe19127d4179131b1 Mon Sep 17 00:00:00 2001 From: Mahadev Annabhimoju <219508079+Joosboy@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:05:30 +0530 Subject: [PATCH 03/40] docs(everything): fix markdown formatting and instructions file references --- src/everything/README.md | 2 +- src/everything/docs/startup.md | 2 +- src/everything/docs/structure.md | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/everything/README.md b/src/everything/README.md index a5943c6..f061e94 100644 --- a/src/everything/README.md +++ b/src/everything/README.md @@ -113,7 +113,7 @@ npm run start:streamableHttp ### Install ```shell npm install -g @modelcontextprotocol/server-everything@latest -```` +``` ### Run the default (stdio) server ```shell diff --git a/src/everything/docs/startup.md b/src/everything/docs/startup.md index 1d00658..5d70e98 100644 --- a/src/everything/docs/startup.md +++ b/src/everything/docs/startup.md @@ -54,7 +54,7 @@ - `prompts: {}` - `resources: { subscribe: true }` - **Server Instructions** - - Loaded from the docs folder (`server-instructions.md`). + - Loaded from the docs folder (`instructions.md`). - **Registrations** - Registers **tools** via `registerTools(server)`. - Registers **resources** via `registerResources(server)`. diff --git a/src/everything/docs/structure.md b/src/everything/docs/structure.md index bd3d70b..ff1e67d 100644 --- a/src/everything/docs/structure.md +++ b/src/everything/docs/structure.md @@ -85,9 +85,19 @@ src/everything ### `docs/` - `architecture.md` - - This document. + - Describes the server architecture, including transport layers, session initialization, and primitive registration. +- `extension.md` + - Explains how to extend the server with new tools, prompts, resources, or transports. +- `features.md` + - A comprehensive reference listing all implemented prompts, resources, tools, and protocol behaviors. +- `how-it-works.md` + - Explains how SSE and Streamable HTTP transports are set up and how sessions are managed. - `instructions.md` - Human‑readable instructions intended to be passed to the client/LLM as guidance on server use. Loaded by the server at startup and returned in the initialize exchange. +- `startup.md` + - Describes the server startup sequence, environment checks, and initialization. +- `structure.md` + - This document. ### `prompts/` From 31bd12967f4bef65cc7ae9b1e72ab4685be14756 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:02:03 +0000 Subject: [PATCH 04/40] chore(deps): bump actions/setup-python from 6 to 7 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/python.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 0c6f091..99bc457 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -39,7 +39,7 @@ jobs: uses: astral-sh/setup-uv@v3 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version-file: "src/${{ matrix.package }}/.python-version" @@ -76,7 +76,7 @@ jobs: uses: astral-sh/setup-uv@v3 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version-file: "src/${{ matrix.package }}/.python-version" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f02a263..bde187a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -124,7 +124,7 @@ jobs: uses: astral-sh/setup-uv@v5 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version-file: "src/${{ matrix.package }}/.python-version" From 40ce2f784e5c5c4a59f64499f709975dd7c2bae3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:02:06 +0000 Subject: [PATCH 05/40] chore(deps): bump actions/upload-artifact from 6 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/python.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 0c6f091..f7d70e9 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -93,7 +93,7 @@ jobs: run: uv build - name: Upload artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: dist-${{ matrix.package }} path: src/${{ matrix.package }}/dist/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f02a263..379676d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: cat RELEASE_NOTES.md - name: Release notes - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: release-notes path: RELEASE_NOTES.md From 42a1d825b138d7ce2b55489a038ee3651cc78ac5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:02:08 +0000 Subject: [PATCH 06/40] chore(deps): bump actions/download-artifact from 7 to 8 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f02a263..9ae0862 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -228,7 +228,7 @@ jobs: - uses: actions/checkout@v6 - name: Download release notes - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: release-notes From 2e15194d4e249bca0998714c2cf016ab2308f99e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:02:12 +0000 Subject: [PATCH 07/40] chore(deps): bump astral-sh/setup-uv from 3 to 7 Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 7. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v7) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/python.yml | 4 ++-- .github/workflows/release.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 0c6f091..9e8c6bf 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@v6 - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v7 - name: Set up Python uses: actions/setup-python@v6 @@ -73,7 +73,7 @@ jobs: - uses: actions/checkout@v6 - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v7 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f02a263..5d90714 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: echo "Using last release hash: ${HASH}" - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Create version name id: create-version @@ -77,7 +77,7 @@ jobs: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Update packages run: | @@ -121,7 +121,7 @@ jobs: ref: ${{ needs.create-metadata.outputs.version }} - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Set up Python uses: actions/setup-python@v6 From fab6abed5888cbddb54a2ff89018221f12d4e1ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:02:15 +0000 Subject: [PATCH 08/40] chore(deps): bump actions/setup-node from 6 to 7 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- .github/workflows/typescript.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f02a263..e31fa8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -173,7 +173,7 @@ jobs: with: ref: ${{ needs.create-metadata.outputs.version }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 22 cache: npm diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index 8d1b81e..d9594b4 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -34,7 +34,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 22 cache: npm @@ -57,7 +57,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 22 cache: npm From fd8248e56b822cc16a74b6610a88141fdfa09bd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:04:42 +0000 Subject: [PATCH 09/40] chore(deps): bump the npm_and_yarn group across 1 directory with 6 updates Bumps the npm_and_yarn group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@hono/node-server](https://github.com/honojs/node-server) | `1.19.14` | `2.0.12` | | [body-parser](https://github.com/expressjs/body-parser) | `2.2.2` | `2.3.0` | | [brace-expansion](https://github.com/juliangruber/brace-expansion) | `5.0.6` | `5.0.8` | | [fast-uri](https://github.com/fastify/fast-uri) | `3.1.2` | `3.1.4` | | [hono](https://github.com/honojs/hono) | `4.12.25` | `4.12.32` | | [postcss](https://github.com/postcss/postcss) | `8.5.15` | `8.5.25` | Updates `@hono/node-server` from 1.19.14 to 2.0.12 - [Release notes](https://github.com/honojs/node-server/releases) - [Commits](https://github.com/honojs/node-server/compare/v1.19.14...v2.0.12) Updates `body-parser` from 2.2.2 to 2.3.0 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/v2.2.2...v2.3.0) Updates `brace-expansion` from 5.0.6 to 5.0.8 - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.6...v5.0.8) Updates `fast-uri` from 3.1.2 to 3.1.4 - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.4) Updates `hono` from 4.12.25 to 4.12.32 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.12.25...v4.12.32) Updates `postcss` from 8.5.15 to 8.5.25 - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.15...8.5.25) --- updated-dependencies: - dependency-name: "@hono/node-server" dependency-version: 2.0.12 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: body-parser dependency-version: 2.3.0 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: brace-expansion dependency-version: 5.0.8 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: fast-uri dependency-version: 3.1.4 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: hono dependency-version: 4.12.32 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: postcss dependency-version: 8.5.25 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 997 ++++++++++++++-------------- src/everything/package.json | 4 +- src/filesystem/package.json | 6 +- src/memory/package.json | 4 +- src/sequentialthinking/package.json | 4 +- 5 files changed, 523 insertions(+), 492 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26261a7..0aab4da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,23 +124,6 @@ "hono": "^4" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -170,12 +153,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -244,6 +227,44 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@oxc-project/types": { "version": "0.133.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", @@ -254,16 +275,6 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -900,30 +911,6 @@ } } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -956,20 +943,20 @@ } }, "node_modules/body-parser": { - "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==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -979,16 +966,42 @@ "url": "https://opencollective.com/express" } }, + "node_modules/body-parser/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/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/bytes": { @@ -1156,13 +1169,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -1306,24 +1312,12 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1333,6 +1327,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1425,6 +1429,98 @@ "node": ">=18.0.0" } }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -1502,10 +1598,27 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "funding": [ { "type": "github", @@ -1518,6 +1631,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1536,6 +1659,19 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -1557,22 +1693,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1591,13 +1711,6 @@ "node": ">= 0.8" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1668,57 +1781,49 @@ "node": ">= 0.4" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" + "pump": "^3.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1766,9 +1871,9 @@ } }, "node_modules/hono": { - "version": "4.12.25", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", - "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "version": "4.12.32", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", + "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -1823,18 +1928,6 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1885,6 +1978,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1894,12 +1997,45 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1951,21 +2087,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jose": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", @@ -2277,10 +2398,13 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/magic-string": { "version": "0.30.21", @@ -2350,6 +2474,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -2416,9 +2577,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -2443,6 +2604,36 @@ "node": ">= 0.6" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2499,11 +2690,15 @@ "wrappy": "1" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, "node_modules/pako": { "version": "1.0.11", @@ -2520,16 +2715,6 @@ "node": ">= 0.8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2547,16 +2732,16 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2609,9 +2794,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -2629,7 +2814,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2672,6 +2857,17 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -2687,6 +2883,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2778,6 +2995,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rolldown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", @@ -2828,6 +3056,30 @@ "node": ">= 18" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2932,13 +3184,14 @@ } }, "node_modules/shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", + "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "glob": "^7.0.0", + "execa": "^1.0.0", + "fast-glob": "^3.3.2", "interpret": "^1.0.0", "rechoir": "^0.6.2" }, @@ -2946,77 +3199,24 @@ "shjs": "bin/shjs" }, "engines": { - "node": ">=4" - } - }, - "node_modules/shelljs/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/shelljs/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/shelljs/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/shelljs/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": ">=18" } }, "node_modules/shx": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", - "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.4.0.tgz", + "integrity": "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.3", - "shelljs": "^0.8.5" + "minimist": "^1.2.8", + "shelljs": "^0.9.2" }, "bin": { "shx": "lib/cli.js" }, "engines": { - "node": ">=6" + "node": ">=18" } }, "node_modules/side-channel": { @@ -3099,16 +3299,11 @@ "license": "ISC" }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" }, "node_modules/source-map-js": { "version": "1.2.1", @@ -3152,100 +3347,14 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/supports-color": { @@ -3318,6 +3427,19 @@ "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -3611,97 +3733,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3808,7 +3839,7 @@ "version": "2.0.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.30.0", "cors": "^2.8.5", "express": "^5.2.1", "jszip": "^3.10.1", @@ -3822,7 +3853,7 @@ "@types/express": "^5.0.6", "@vitest/coverage-v8": "^4.1.8", "prettier": "^2.8.8", - "shx": "^0.3.4", + "shx": "^0.4.0", "typescript": "^5.6.2", "vitest": "^4.1.8" } @@ -3832,9 +3863,9 @@ "version": "0.6.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.30.0", "diff": "^8.0.3", - "glob": "^10.5.0", + "glob": "^13.0.6", "minimatch": "^10.0.1" }, "bin": { @@ -3845,7 +3876,7 @@ "@types/minimatch": "^5.1.2", "@types/node": "^22", "@vitest/coverage-v8": "^4.1.8", - "shx": "^0.3.4", + "shx": "^0.4.0", "typescript": "^5.8.2", "vitest": "^4.1.8" } @@ -3855,7 +3886,7 @@ "version": "0.6.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/sdk": "^1.30.0" }, "bin": { "mcp-server-memory": "dist/index.js" @@ -3863,7 +3894,7 @@ "devDependencies": { "@types/node": "^22", "@vitest/coverage-v8": "^4.1.8", - "shx": "^0.3.4", + "shx": "^0.4.0", "typescript": "^5.6.2", "vitest": "^4.1.8" } @@ -3873,7 +3904,7 @@ "version": "0.6.2", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.30.0", "chalk": "^5.3.0", "yargs": "^17.7.2" }, @@ -3884,7 +3915,7 @@ "@types/node": "^22", "@types/yargs": "^17.0.32", "@vitest/coverage-v8": "^4.1.8", - "shx": "^0.3.4", + "shx": "^0.4.0", "typescript": "^5.3.3", "vitest": "^4.1.8" } diff --git a/src/everything/package.json b/src/everything/package.json index 4f8b836..745d1b1 100644 --- a/src/everything/package.json +++ b/src/everything/package.json @@ -30,7 +30,7 @@ "test": "vitest run --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.30.0", "cors": "^2.8.5", "express": "^5.2.1", "jszip": "^3.10.1", @@ -41,7 +41,7 @@ "@types/express": "^5.0.6", "@vitest/coverage-v8": "^4.1.8", "prettier": "^2.8.8", - "shx": "^0.3.4", + "shx": "^0.4.0", "typescript": "^5.6.2", "vitest": "^4.1.8" } diff --git a/src/filesystem/package.json b/src/filesystem/package.json index c943ce2..139c4f0 100644 --- a/src/filesystem/package.json +++ b/src/filesystem/package.json @@ -25,9 +25,9 @@ "test": "vitest run --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.30.0", "diff": "^8.0.3", - "glob": "^10.5.0", + "glob": "^13.0.6", "minimatch": "^10.0.1" }, "devDependencies": { @@ -35,7 +35,7 @@ "@types/minimatch": "^5.1.2", "@types/node": "^22", "@vitest/coverage-v8": "^4.1.8", - "shx": "^0.3.4", + "shx": "^0.4.0", "typescript": "^5.8.2", "vitest": "^4.1.8" } diff --git a/src/memory/package.json b/src/memory/package.json index e32d25a..4fdcd3f 100644 --- a/src/memory/package.json +++ b/src/memory/package.json @@ -25,12 +25,12 @@ "test": "vitest run --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/sdk": "^1.30.0" }, "devDependencies": { "@types/node": "^22", "@vitest/coverage-v8": "^4.1.8", - "shx": "^0.3.4", + "shx": "^0.4.0", "typescript": "^5.6.2", "vitest": "^4.1.8" } diff --git a/src/sequentialthinking/package.json b/src/sequentialthinking/package.json index a88de80..03fbb2b 100644 --- a/src/sequentialthinking/package.json +++ b/src/sequentialthinking/package.json @@ -25,7 +25,7 @@ "test": "vitest run --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.30.0", "chalk": "^5.3.0", "yargs": "^17.7.2" }, @@ -33,7 +33,7 @@ "@types/node": "^22", "@types/yargs": "^17.0.32", "@vitest/coverage-v8": "^4.1.8", - "shx": "^0.3.4", + "shx": "^0.4.0", "typescript": "^5.3.3", "vitest": "^4.1.8" } From 3bcfedd8e40212a1fd6e4bd3ab5abf459257f83b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:05:20 +0000 Subject: [PATCH 10/40] chore(deps): bump the uv group across 3 directories with 4 updates Bumps the uv group with 3 updates in the /src/git directory: [gitpython](https://github.com/gitpython-developers/GitPython), [mcp](https://github.com/modelcontextprotocol/python-sdk) and [pydantic-settings](https://github.com/pydantic/pydantic-settings). Bumps the uv group with 2 updates in the /src/time directory: [mcp](https://github.com/modelcontextprotocol/python-sdk) and [pydantic-settings](https://github.com/pydantic/pydantic-settings). Bumps the uv group with 2 updates in the /src/fetch directory: [mcp](https://github.com/modelcontextprotocol/python-sdk) and [soupsieve](https://github.com/facelessuser/soupsieve). Updates `gitpython` from 3.1.50 to 3.1.54 - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.50...3.1.54) Updates `mcp` from 1.23.0 to 1.28.1 - [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md) - [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.23.0...v1.28.1) Updates `pydantic-settings` from 2.12.0 to 2.14.2 - [Release notes](https://github.com/pydantic/pydantic-settings/releases) - [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.12.0...v2.14.2) Updates `mcp` from 1.25.0 to 1.28.1 - [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md) - [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.23.0...v1.28.1) Updates `pydantic-settings` from 2.12.0 to 2.14.2 - [Release notes](https://github.com/pydantic/pydantic-settings/releases) - [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.12.0...v2.14.2) Updates `mcp` from 1.23.0 to 1.28.1 - [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md) - [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.23.0...v1.28.1) Updates `soupsieve` from 2.6 to 2.8.4 - [Release notes](https://github.com/facelessuser/soupsieve/releases) - [Commits](https://github.com/facelessuser/soupsieve/compare/2.6...2.8.4) --- updated-dependencies: - dependency-name: gitpython dependency-version: 3.1.54 dependency-type: direct:production dependency-group: uv - dependency-name: mcp dependency-version: 1.28.1 dependency-type: direct:production dependency-group: uv - dependency-name: pydantic-settings dependency-version: 2.14.2 dependency-type: indirect dependency-group: uv - dependency-name: mcp dependency-version: 1.28.1 dependency-type: direct:production dependency-group: uv - dependency-name: pydantic-settings dependency-version: 2.14.2 dependency-type: indirect dependency-group: uv - dependency-name: mcp dependency-version: 1.28.1 dependency-type: direct:production dependency-group: uv - dependency-name: soupsieve dependency-version: 2.8.4 dependency-type: indirect dependency-group: uv ... Signed-off-by: dependabot[bot] --- src/fetch/uv.lock | 18 ++++++++++++------ src/git/uv.lock | 24 +++++++++++++++--------- src/time/uv.lock | 18 ++++++++++++------ 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/fetch/uv.lock b/src/fetch/uv.lock index ea9b056..d79e776 100644 --- a/src/fetch/uv.lock +++ b/src/fetch/uv.lock @@ -1,6 +1,12 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] [[package]] name = "annotated-types" @@ -533,7 +539,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.23.0" +version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -551,9 +557,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/1a/9c8a5362e3448d585081d6c7aa95898a64e0ac59d3e26169ae6c3ca5feaf/mcp-1.23.0.tar.gz", hash = "sha256:84e0c29316d0a8cf0affd196fd000487ac512aa3f771b63b2ea864e22961772b", size = 596506, upload-time = "2025-12-02T13:40:02.558Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/b2/28739ce409f98159c0121eab56e69ad71546c4f34ac8b42e58c03f57dccc/mcp-1.23.0-py3-none-any.whl", hash = "sha256:5a645cf111ed329f4619f2629a3f15d9aabd7adc2ea09d600d31467b51ecb64f", size = 231427, upload-time = "2025-12-02T13:40:00.738Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] [[package]] @@ -1179,11 +1185,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.6" +version = "2.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569, upload-time = "2024-08-13T13:39:12.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186, upload-time = "2024-08-13T13:39:10.986Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] [[package]] diff --git a/src/git/uv.lock b/src/git/uv.lock index a838e3d..517aa87 100644 --- a/src/git/uv.lock +++ b/src/git/uv.lock @@ -1,6 +1,12 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] [[package]] name = "annotated-types" @@ -227,14 +233,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.50" +version = "3.1.54" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/d5/3da0b92033887033f4c27f2dd109a303c4ca62813c7b3bb2511edb4777de/gitpython-3.1.54.tar.gz", hash = "sha256:53f2085e24a2cda300eed7c3fc5f1559ae289634b725e98acaf4791940247aa0", size = 225076, upload-time = "2026-07-22T04:08:51.403Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b9/876f442a28df5c068ca69b0122d5c35e65fd2d2fa9992ea5cb5944ea00a6/gitpython-3.1.54-py3-none-any.whl", hash = "sha256:b90d7b3d9bc0238681d24369130826f0dcdb0ceaa45db67cf1d4ffa4c302dedf", size = 216575, upload-time = "2026-07-22T04:08:50.05Z" }, ] [[package]] @@ -331,7 +337,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.23.0" +version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -349,9 +355,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/1a/9c8a5362e3448d585081d6c7aa95898a64e0ac59d3e26169ae6c3ca5feaf/mcp-1.23.0.tar.gz", hash = "sha256:84e0c29316d0a8cf0affd196fd000487ac512aa3f771b63b2ea864e22961772b", size = 596506, upload-time = "2025-12-02T13:40:02.558Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/b2/28739ce409f98159c0121eab56e69ad71546c4f34ac8b42e58c03f57dccc/mcp-1.23.0-py3-none-any.whl", hash = "sha256:5a645cf111ed329f4619f2629a3f15d9aabd7adc2ea09d600d31467b51ecb64f", size = 231427, upload-time = "2025-12-02T13:40:00.738Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] [[package]] @@ -558,16 +564,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] diff --git a/src/time/uv.lock b/src/time/uv.lock index e77b3c3..072d962 100644 --- a/src/time/uv.lock +++ b/src/time/uv.lock @@ -1,6 +1,12 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] [[package]] name = "annotated-types" @@ -318,7 +324,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.25.0" +version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -336,9 +342,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] [[package]] @@ -547,16 +553,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] From d8cff7f016604a1edc6c362e00f3f5203fdba6c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:07:16 +0000 Subject: [PATCH 11/40] chore(deps): bump @hono/node-server Bumps the npm_and_yarn group with 1 update in the / directory: [@hono/node-server](https://github.com/honojs/node-server). Updates `@hono/node-server` from 1.19.14 to 2.0.12 - [Release notes](https://github.com/honojs/node-server/releases) - [Commits](https://github.com/honojs/node-server/compare/v1.19.14...v2.0.12) --- updated-dependencies: - dependency-name: "@hono/node-server" dependency-version: 2.0.12 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0aab4da..1845571 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,12 +113,12 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" From 53dbe83b7a9ee1f597179ea395962383d333eac9 Mon Sep 17 00:00:00 2001 From: olaservo Date: Tue, 18 Aug 2026 08:26:30 -0700 Subject: [PATCH 12/40] fix(python): require mcp>=1.29.0,<2 in fetch, git, and time mcp 2.0.0 renamed McpError to MCPError and replaced the low-level Server decorators with constructor handlers. The three servers had no upper bound, so a fresh uvx install has resolved 2.0.0 since 2026-07-28 and failed to start: fetch and time raise ImportError on the McpError import, git raises AttributeError on server.list_tools(). The floor moves to 1.29.0, the last 1.x release. The previous floors (1.0.0 for git, 1.1.3 for fetch, 1.23.0 for time) resolved versions affected by GHSA-vj7q-gjh5-988w, GHSA-jpw9-pfvf-9f58, and GHSA-hvrp-rf83-w775. Porting to the v2 SDK is tracked separately. Closes #4560 Closes #4570 Closes #4580 Co-Authored-By: Claude Opus 5 --- src/fetch/pyproject.toml | 2 +- src/fetch/uv.lock | 8 ++++---- src/git/pyproject.toml | 2 +- src/git/uv.lock | 8 ++++---- src/time/pyproject.toml | 2 +- src/time/uv.lock | 8 ++++---- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/fetch/pyproject.toml b/src/fetch/pyproject.toml index 84735f2..ee97fc8 100644 --- a/src/fetch/pyproject.toml +++ b/src/fetch/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ dependencies = [ "httpx>=0.27", "markdownify>=0.13.1", - "mcp>=1.1.3", + "mcp>=1.29.0,<2", "protego>=0.3.1", "pydantic>=2.0.0", "readabilipy>=0.2.0", diff --git a/src/fetch/uv.lock b/src/fetch/uv.lock index d79e776..f35a318 100644 --- a/src/fetch/uv.lock +++ b/src/fetch/uv.lock @@ -539,7 +539,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.28.1" +version = "1.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -557,9 +557,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, ] [[package]] @@ -588,7 +588,7 @@ dev = [ requires-dist = [ { name = "httpx", specifier = ">=0.27" }, { name = "markdownify", specifier = ">=0.13.1" }, - { name = "mcp", specifier = ">=1.1.3" }, + { name = "mcp", specifier = ">=1.29.0,<2" }, { name = "protego", specifier = ">=0.3.1" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "readabilipy", specifier = ">=0.2.0" }, diff --git a/src/git/pyproject.toml b/src/git/pyproject.toml index c5113ae..e2407ba 100644 --- a/src/git/pyproject.toml +++ b/src/git/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ dependencies = [ "click>=8.1.7", "gitpython>=3.1.50", - "mcp>=1.0.0", + "mcp>=1.29.0,<2", "pydantic>=2.0.0", ] diff --git a/src/git/uv.lock b/src/git/uv.lock index 517aa87..2ddb41d 100644 --- a/src/git/uv.lock +++ b/src/git/uv.lock @@ -337,7 +337,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.28.1" +version = "1.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -355,9 +355,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, ] [[package]] @@ -382,7 +382,7 @@ dev = [ requires-dist = [ { name = "click", specifier = ">=8.1.7" }, { name = "gitpython", specifier = ">=3.1.50" }, - { name = "mcp", specifier = ">=1.0.0" }, + { name = "mcp", specifier = ">=1.29.0,<2" }, { name = "pydantic", specifier = ">=2.0.0" }, ] diff --git a/src/time/pyproject.toml b/src/time/pyproject.toml index f9be050..e6400d5 100644 --- a/src/time/pyproject.toml +++ b/src/time/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", ] dependencies = [ - "mcp>=1.23.0", + "mcp>=1.29.0,<2", "pydantic>=2.0.0", "tzdata>=2024.2", "tzlocal>=5.3.1", diff --git a/src/time/uv.lock b/src/time/uv.lock index 072d962..542b996 100644 --- a/src/time/uv.lock +++ b/src/time/uv.lock @@ -324,7 +324,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.28.1" +version = "1.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -342,9 +342,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, ] [[package]] @@ -368,7 +368,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "mcp", specifier = ">=1.23.0" }, + { name = "mcp", specifier = ">=1.29.0,<2" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "tzdata", specifier = ">=2024.2" }, { name = "tzlocal", specifier = ">=5.3.1" }, From d811ba11f1243a55bcc2e78d72ddd9b63b25f25e Mon Sep 17 00:00:00 2001 From: olaservo Date: Tue, 18 Aug 2026 08:57:03 -0700 Subject: [PATCH 13/40] docs(python): note the mcp 1.x requirement in fetch, git, and time Users hitting the 2.0.0 startup failure land on the README first. Also makes the three packages qualify for the next release: the release workflow selects packages by .py/.ts/.md changes, so a dependency-only fix would merge without publishing. Co-Authored-By: Claude Opus 5 --- src/fetch/README.md | 2 ++ src/git/README.md | 2 ++ src/time/README.md | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/fetch/README.md b/src/fetch/README.md index ed6d226..7e28699 100644 --- a/src/fetch/README.md +++ b/src/fetch/README.md @@ -6,6 +6,8 @@ A Model Context Protocol server that provides web content fetching capabilities. Source: https://github.com/modelcontextprotocol/servers/tree/main/src/fetch +Requires MCP Python SDK 1.x (`mcp>=1.29.0,<2`). SDK 2.0 renamed APIs this server uses. The port to v2 is in progress. + > [!CAUTION] > This server can access local/internal IP addresses and may represent a security risk. Exercise caution when using this MCP server to ensure this does not expose any sensitive data. diff --git a/src/git/README.md b/src/git/README.md index a252a4a..70b0097 100644 --- a/src/git/README.md +++ b/src/git/README.md @@ -8,6 +8,8 @@ A Model Context Protocol server for Git repository interaction and automation. T Source: https://github.com/modelcontextprotocol/servers/tree/main/src/git +Requires MCP Python SDK 1.x (`mcp>=1.29.0,<2`). SDK 2.0 renamed APIs this server uses. The port to v2 is in progress. + Please note that mcp-server-git is currently in early development. The functionality and available tools are subject to change and expansion as we continue to develop and improve the server. ### Tools diff --git a/src/time/README.md b/src/time/README.md index 9adbe19..9a2c1c4 100644 --- a/src/time/README.md +++ b/src/time/README.md @@ -6,6 +6,8 @@ A Model Context Protocol server that provides time and timezone conversion capab Source: https://github.com/modelcontextprotocol/servers/tree/main/src/time +Requires MCP Python SDK 1.x (`mcp>=1.29.0,<2`). SDK 2.0 renamed APIs this server uses. The port to v2 is in progress. + ### Available Tools - `get_current_time` - Get current time in a specific timezone or system timezone. From 8102fbe27a92535754cf44d42987af150b08f105 Mon Sep 17 00:00:00 2001 From: nulone <115600674+nulone@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:17:22 +0700 Subject: [PATCH 14/40] fix(filesystem): handle negative values in formatSize() (#3231) Return '0 B' for negative byte values instead of 'NaN B'. File sizes cannot be negative, so this is a safe default. Co-authored-by: Claude Opus 4.5 --- src/filesystem/__tests__/lib.test.ts | 6 ++++-- src/filesystem/lib.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index e0ae612..da1adf2 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -65,8 +65,10 @@ describe('Lib Functions', () => { }); it('handles negative numbers', () => { - // Negative numbers will result in NaN for the log calculation - expect(formatSize(-1024)).toContain('NaN'); + // Negative numbers should return '0 B' as file sizes cannot be negative + expect(formatSize(-1)).toBe('0 B'); + expect(formatSize(-1024)).toBe('0 B'); + expect(formatSize(-1000000)).toBe('0 B'); expect(formatSize(-0)).toBe('0 B'); }); diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index ce4af9f..57b93a0 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -43,7 +43,7 @@ export interface SearchResult { // Pure Utility Functions export function formatSize(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) return '0 B'; + if (bytes <= 0) return '0 B'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); From 781a93db14c430b9410d89910f42a65a3c9d0c72 Mon Sep 17 00:00:00 2001 From: 1060996408 Date: Fri, 28 Aug 2026 10:17:27 +0800 Subject: [PATCH 15/40] fix(everything): remove dead code in template URI validation and subscription cleanup (#4104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resources/templates.ts: - `parseResourceId` had a guard that compared the URI against both `textUriBase` and `blobUriBase` with `&&`. Those prefixes are mutually exclusive, so the condition is always false and the branch is dead. Drop it; the SDK's template-based routing already guarantees the URI prefix is one of the two before the handler runs. The remaining positive-integer check on `resourceId` is preserved. resources/subscriptions.ts: - `sendSimulatedResourceUpdates` had an `else` branch that called `subscribers.delete(sessionId)` whenever the session wasn't in a URI's subscriber set, with a comment claiming the session had disconnected. That conclusion doesn't follow — a session not subscribed to URI A can still be subscribed to URI B — and the delete is a no-op when the element is absent anyway. Remove the branch. No behavioral change. Existing 95 / 95 tests in `__tests__` still pass. Co-authored-by: Jia Xuan <1060996408+jiaxuan@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) --- src/everything/resources/subscriptions.ts | 9 ++---- src/everything/resources/templates.ts | 37 ++++++++++------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/everything/resources/subscriptions.ts b/src/everything/resources/subscriptions.ts index 2a5e574..854a863 100644 --- a/src/everything/resources/subscriptions.ts +++ b/src/everything/resources/subscriptions.ts @@ -99,10 +99,9 @@ export const setSubscriptionHandlers = (server: McpServer) => { /** * Sends simulated resource update notifications to the subscribed client. * - * This function iterates through all resource URIs stored in the subscriptions - * and checks if the specified session ID is subscribed to them. If so, it sends - * a notification through the provided server. If the session ID is no longer valid - * (disconnected), it removes the session ID from the list of subscribers. + * Iterates the URIs in `subscriptions` and emits a + * `notifications/resources/updated` notification for each URI the given + * sessionId is subscribed to. * * @param {McpServer} server - The server instance used to send notifications. * @param {string | undefined} sessionId - The session ID of the client to check for subscriptions. @@ -122,8 +121,6 @@ const sendSimulatedResourceUpdates = async ( method: "notifications/resources/updated", params: { uri }, }); - } else { - subscribers.delete(sessionId); // subscriber has disconnected } } }; diff --git a/src/everything/resources/templates.ts b/src/everything/resources/templates.ts index 6d4903f..4530741 100644 --- a/src/everything/resources/templates.ts +++ b/src/everything/resources/templates.ts @@ -127,31 +127,26 @@ export const blobResourceUri = (resourceId: number) => new URL(`${blobUriBase}/${resourceId}`); /** - * Parses the resource identifier from the provided URI and validates it - * against the given variables. Throws an error if the URI corresponds - * to an unknown resource or if the resource identifier is invalid. + * Parses the resource identifier from the provided variables and validates + * that it is a positive integer. * - * @param {URL} uri - The URI of the resource to be parsed. - * @param {Record} variables - A record containing context-specific variables that include the resourceId. - * @returns {number} The parsed and validated resource identifier as an integer. - * @throws {Error} Throws an error if the URI matches unsupported base URIs or if the resourceId is invalid. + * The SDK only routes URIs that match the registered template to the + * resource handler, so by the time we get here the URI prefix is already + * known to be one of `textUriBase` / `blobUriBase`. Only the resourceId + * variable still needs validating. + * + * @param {URL} uri - The URI of the resource (used in the error message). + * @param {Record} variables - Context variables including resourceId. + * @returns {number} The parsed and validated resource identifier as a positive integer. + * @throws {Error} If the resourceId is not a finite positive integer. */ const parseResourceId = (uri: URL, variables: Record) => { - const uriError = `Unknown resource: ${uri.toString()}`; - if ( - uri.toString().startsWith(textUriBase) && - uri.toString().startsWith(blobUriBase) - ) { - throw new Error(uriError); - } else { - const idxStr = String((variables as any).resourceId ?? ""); - const idx = Number(idxStr); - if (Number.isFinite(idx) && Number.isInteger(idx) && idx > 0) { - return idx; - } else { - throw new Error(uriError); - } + const idxStr = String((variables as any).resourceId ?? ""); + const idx = Number(idxStr); + if (Number.isFinite(idx) && Number.isInteger(idx) && idx > 0) { + return idx; } + throw new Error(`Unknown resource: ${uri.toString()}`); }; /** From 71e3cfbd1e7e0ff88fa24ce5406f8e220ceae5c4 Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Thu, 27 Aug 2026 19:17:32 -0700 Subject: [PATCH 16/40] test(filesystem): add MCP SDK regression coverage for directory_tree (#3245) * test(filesystem): add directory_tree MCP SDK regression coverage * docs(filesystem): drop troubleshooting note from README --- .../__tests__/directory-tree.mcp-sdk.test.ts | 57 +++++++++++++++++++ .../__tests__/structured-content.test.ts | 4 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/filesystem/__tests__/directory-tree.mcp-sdk.test.ts diff --git a/src/filesystem/__tests__/directory-tree.mcp-sdk.test.ts b/src/filesystem/__tests__/directory-tree.mcp-sdk.test.ts new file mode 100644 index 0000000..0cbe48c --- /dev/null +++ b/src/filesystem/__tests__/directory-tree.mcp-sdk.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as os from 'os'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +describe('directory_tree MCP SDK regression', () => { + let client: Client; + let transport: StdioClientTransport; + let testDir: string; + + beforeEach(async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-tree-')); + testDir = await fs.realpath(tmp); + + await fs.writeFile(path.join(testDir, 'root.txt'), 'root'); + await fs.mkdir(path.join(testDir, 'nested')); + await fs.writeFile(path.join(testDir, 'nested', 'child.txt'), 'child'); + + const serverPath = path.resolve(__dirname, '../dist/index.js'); + transport = new StdioClientTransport({ + command: 'node', + args: [serverPath, testDir], + }); + + client = new Client( + { name: 'directory-tree-regression-test', version: '1.0.0' }, + { capabilities: {} }, + ); + + await client.connect(transport); + }); + + afterEach(async () => { + await client?.close(); + await fs.rm(testDir, { recursive: true, force: true }); + }); + + it('returns structuredContent.content as a string (not an array) when called via MCP SDK', async () => { + const result = await client.callTool({ + name: 'directory_tree', + arguments: { path: testDir }, + }); + + // Regression test for issues where structuredContent was returned as an array, + // which causes MCP SDK validation to throw -32602 (invalid structured content). + const structured = result.structuredContent as { content: unknown }; + expect(structured).toBeDefined(); + expect(typeof structured.content).toBe('string'); + expect(Array.isArray(structured.content)).toBe(false); + + const parsed = JSON.parse(structured.content as string); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.length).toBeGreaterThan(0); + }); +}); diff --git a/src/filesystem/__tests__/structured-content.test.ts b/src/filesystem/__tests__/structured-content.test.ts index 4605b72..67da16f 100644 --- a/src/filesystem/__tests__/structured-content.test.ts +++ b/src/filesystem/__tests__/structured-content.test.ts @@ -22,7 +22,9 @@ describe('structuredContent schema compliance', () => { beforeEach(async () => { // Create a temp directory for testing - testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-test-')); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-test-')); + // macOS temp dirs may be symlinked (/var -> /private/var); resolve for server allowlist checks. + testDir = await fs.realpath(tmpDir); // Create test files await fs.writeFile(path.join(testDir, 'test.txt'), 'test content'); From 562feeb2817266e0d155b9e4bff54a0233557f12 Mon Sep 17 00:00:00 2001 From: drudiger Date: Thu, 27 Aug 2026 19:36:45 -0700 Subject: [PATCH 17/40] fix(filesystem): preserve file permissions during write and edit operations (#4115) * fix(filesystem): preserve file permissions during write and edit operations The atomic write pattern (write temp file + rename) replaces the original inode, causing the new file to have default 0644 permissions regardless of what the original file had. This breaks executable scripts and other files with non-default permissions. Fix: capture stat.mode before writing and restore it with chmod after rename. Fixes both writeFileContent() and applyFileEdits(). * fix(filesystem): mask chmod mode and keep chmod failure from failing the write Pass origStats.mode & 0o777 to chmod instead of the full st_mode. Move the chmod out of the try whose catch unlinks the temp file and rethrows, since rename has already succeeded by then. Add a test that an EPERM from chmod does not reject the write. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018Wo28CHPXyM3DHoLWvKnCK --------- Co-authored-by: Dustin Rudiger Co-authored-by: olaservo Co-authored-by: Claude Fable 5 --- src/filesystem/__tests__/lib.test.ts | 53 ++++++++++++++++++++++++++++ src/filesystem/lib.ts | 16 +++++++++ 2 files changed, 69 insertions(+) diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index da1adf2..1da741f 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -310,6 +310,33 @@ describe('Lib Functions', () => { expect(mockFs.writeFile).toHaveBeenCalledWith('/test/file.txt', 'new content', { encoding: "utf-8", flag: 'wx' }); }); + + it('preserves file permissions when overwriting existing file', async () => { + // First writeFile call with 'wx' flag fails because file exists + mockFs.writeFile.mockRejectedValueOnce(Object.assign(new Error('EEXIST'), { code: 'EEXIST' })); + // stat returns executable permissions + mockFs.stat.mockResolvedValueOnce({ mode: 0o100755 }); + // Second writeFile (to temp) succeeds + mockFs.writeFile.mockResolvedValueOnce(undefined); + mockFs.rename.mockResolvedValueOnce(undefined); + mockFs.chmod.mockResolvedValueOnce(undefined); + + await writeFileContent('/test/script.sh', 'new content'); + + expect(mockFs.stat).toHaveBeenCalledWith('/test/script.sh'); + expect(mockFs.chmod).toHaveBeenCalledWith('/test/script.sh', 0o755); + }); + + it('does not fail the write when chmod fails', async () => { + mockFs.writeFile.mockRejectedValueOnce(Object.assign(new Error('EEXIST'), { code: 'EEXIST' })); + mockFs.stat.mockResolvedValueOnce({ mode: 0o100755 }); + mockFs.writeFile.mockResolvedValueOnce(undefined); + mockFs.rename.mockResolvedValueOnce(undefined); + mockFs.chmod.mockRejectedValueOnce(Object.assign(new Error('EPERM'), { code: 'EPERM' })); + + await expect(writeFileContent('/test/script.sh', 'new content')).resolves.toBeUndefined(); + expect(mockFs.unlink).not.toHaveBeenCalled(); + }); }); }); @@ -418,6 +445,8 @@ describe('Lib Functions', () => { beforeEach(() => { mockFs.readFile.mockResolvedValue('line1\nline2\nline3\n'); mockFs.writeFile.mockResolvedValue(undefined); + mockFs.stat.mockResolvedValue({ mode: 0o100644 }); + mockFs.chmod.mockResolvedValue(undefined); }); it('applies simple text replacement', async () => { @@ -512,6 +541,30 @@ describe('Lib Functions', () => { ); }); + it('preserves file permissions after applying edits', async () => { + mockFs.stat.mockResolvedValue({ mode: 0o100755 }); + const edits = [ + { oldText: 'line2', newText: 'modified line2' } + ]; + + mockFs.rename.mockResolvedValueOnce(undefined); + + await applyFileEdits('/test/script.sh', edits, false); + + expect(mockFs.stat).toHaveBeenCalledWith('/test/script.sh'); + expect(mockFs.chmod).toHaveBeenCalledWith('/test/script.sh', 0o755); + }); + + it('does not restore permissions in dry run mode', async () => { + const edits = [ + { oldText: 'line2', newText: 'modified line2' } + ]; + + await applyFileEdits('/test/file.txt', edits, true); + + expect(mockFs.chmod).not.toHaveBeenCalled(); + }); + it('throws error for non-matching edits', async () => { const edits = [ { oldText: 'nonexistent line', newText: 'replacement' } diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index 57b93a0..a1c6f04 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -168,6 +168,7 @@ export async function writeFileContent(filePath: string, content: string): Promi // Security: Use atomic rename to prevent race conditions where symlinks // could be created between validation and write. Rename operations // replace the target file atomically and don't follow symlinks. + const origStats = await fs.stat(filePath); const tempPath = `${filePath}.${randomBytes(16).toString('hex')}.tmp`; try { await fs.writeFile(tempPath, content, 'utf-8'); @@ -178,6 +179,13 @@ export async function writeFileContent(filePath: string, content: string): Promi } catch {} throw renameError; } + // Restore original permission bits since the atomic rename replaces the + // inode and the temp file has default (0644) permissions. Mask off the + // file-type bits; POSIX leaves them unspecified for chmod. A chmod + // failure must not fail the write, which has already succeeded. + try { + await fs.chmod(filePath, origStats.mode & 0o777); + } catch {} } else { throw error; } @@ -266,6 +274,7 @@ export async function applyFileEdits( // Security: Use atomic rename to prevent race conditions where symlinks // could be created between validation and write. Rename operations // replace the target file atomically and don't follow symlinks. + const origStats = await fs.stat(filePath); const tempPath = `${filePath}.${randomBytes(16).toString('hex')}.tmp`; try { await fs.writeFile(tempPath, modifiedContent, 'utf-8'); @@ -276,6 +285,13 @@ export async function applyFileEdits( } catch {} throw error; } + // Restore original permission bits since the atomic rename replaces the + // inode and the temp file has default (0644) permissions. Mask off the + // file-type bits; POSIX leaves them unspecified for chmod. A chmod + // failure must not fail the write, which has already succeeded. + try { + await fs.chmod(filePath, origStats.mode & 0o777); + } catch {} } return formattedDiff; From 18ce19763999dcf7697b00c86c3a427f01eb2919 Mon Sep 17 00:00:00 2001 From: eeshsaxena <139802361+eeshsaxena@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:28:51 +0530 Subject: [PATCH 18/40] fix(filesystem): make move_file fail instead of overwriting the destination (#4630) The move_file tool description and README both state the operation fails if the destination already exists, but the handler called fs.rename directly, which silently overwrites the destination. Since this server ships no delete tool, move_file effectively provided unadvertised, irreversible file deletion: moving any file onto an existing target destroyed the target. Add a moveFile helper in lib.ts that rejects when the destination already exists (checked with lstat, so an existing symlink is detected rather than followed) and only renames when the target is free, and use it from the move_file handler. Behaviour now matches the documented contract. Fixes #4628 --- src/filesystem/__tests__/lib.test.ts | 24 ++++++++++++++++++++++++ src/filesystem/index.ts | 3 ++- src/filesystem/lib.ts | 19 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index 1da741f..a3db269 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -14,6 +14,7 @@ import { getFileStats, readFileContent, writeFileContent, + moveFile, // Search & filtering functions searchFilesWithValidation, // File editing functions @@ -339,6 +340,29 @@ describe('Lib Functions', () => { }); }); + describe('moveFile', () => { + it('moves the file when the destination does not exist', async () => { + const enoent = Object.assign(new Error('not found'), { code: 'ENOENT' }); + mockFs.lstat.mockRejectedValueOnce(enoent); + mockFs.rename.mockResolvedValueOnce(undefined); + + await moveFile('/test/source.txt', '/test/dest.txt'); + + expect(mockFs.rename).toHaveBeenCalledWith('/test/source.txt', '/test/dest.txt'); + }); + + it('fails without overwriting when the destination already exists', async () => { + // lstat resolving means the destination is occupied. + mockFs.lstat.mockResolvedValueOnce({} as any); + + await expect(moveFile('/test/source.txt', '/test/dest.txt')).rejects.toThrow( + 'Destination already exists' + ); + + expect(mockFs.rename).not.toHaveBeenCalled(); + }); + }); + }); describe('Search & Filtering Functions', () => { diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 234605b..51ac523 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -21,6 +21,7 @@ import { getFileStats, readFileContent, writeFileContent, + moveFile, searchFilesWithValidation, applyFileEdits, tailFile, @@ -631,7 +632,7 @@ server.registerTool( async (args: z.infer) => { const validSourcePath = await validatePath(args.source); const validDestPath = await validatePath(args.destination); - await fs.rename(validSourcePath, validDestPath); + await moveFile(validSourcePath, validDestPath); const text = `Successfully moved ${args.source} to ${args.destination}`; const contentBlock = { type: "text" as const, text }; return { diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index a1c6f04..3d0b49e 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -193,6 +193,25 @@ export async function writeFileContent(filePath: string, content: string): Promi } +export async function moveFile(sourcePath: string, destinationPath: string): Promise { + // The move_file tool contract (and README) state the operation fails if the + // destination already exists. fs.rename would silently overwrite it, which is + // a data-loss bug, so reject up front when anything - file, directory, or + // symlink - occupies the target. lstat is used so an existing symlink at the + // destination is detected rather than followed. + try { + await fs.lstat(destinationPath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + await fs.rename(sourcePath, destinationPath); + return; + } + throw error; + } + throw new Error(`Destination already exists: ${destinationPath}`); +} + + // File Editing Functions interface FileEdit { oldText: string; From 642a9112e7ae8081c87fa1e2f34f14f5c3e42ab1 Mon Sep 17 00:00:00 2001 From: Teddy100x Date: Sat, 29 Aug 2026 00:00:33 +0700 Subject: [PATCH 19/40] fix(memory): write the knowledge graph atomically (#4642) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(memory): write the knowledge graph atomically `saveGraph()` wrote the graph straight to the memory file: await fs.writeFile(this.memoryFilePath, lines.join("\n")); `fs.writeFile` opens the target with `'w'`, which truncates it before any new bytes are written. If the process is interrupted between truncation and completion — SIGKILL, container or host stop, OOM kill, power loss — the memory file is left empty or half-written. That file is the only persistence layer for the knowledge graph, so the window is small but the loss is total and unrecoverable. The fix writes to a temporary file in the same directory and renames it over the target. `rename(2)` is atomic on POSIX filesystems: a reader sees either the complete old file or the complete new one, never an intermediate state. Keeping the temp file in the same directory ensures the rename stays on one filesystem, since cross-device renames fail with EXDEV. On failure the temp file is removed so no strays accumulate. Tests added in `__tests__/atomic-save.test.ts`: - the live memory file is never opened for truncating writes - a committed graph survives a write that fails midway - no temporary files remain after a successful write - the temporary file is cleaned up when the rename fails - graph contents still round-trip correctly across reloads The first and fourth fail against the previous implementation. Full suite: 55 passed (50 existing, 5 new). `tsc --noEmit` clean. Fixes #4614 * fix(memory): use randomBytes for temp filename to avoid concurrent collision --------- Co-authored-by: teddiesloco --- src/memory/__tests__/atomic-save.test.ts | 133 +++++++++++++++++++++++ src/memory/index.ts | 25 ++++- 2 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/memory/__tests__/atomic-save.test.ts diff --git a/src/memory/__tests__/atomic-save.test.ts b/src/memory/__tests__/atomic-save.test.ts new file mode 100644 index 0000000..8c702b5 --- /dev/null +++ b/src/memory/__tests__/atomic-save.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; +import { KnowledgeGraphManager, Entity } from '../index.js'; + +/** + * Regression tests for durable persistence of the knowledge graph. + * + * saveGraph() previously called fs.writeFile() directly on the memory file. + * fs.writeFile opens the target with 'w', which truncates it before any new + * bytes are written. If the process dies between truncation and completion + * (SIGKILL, container stop, OOM kill, power loss) the memory file — the sole + * persistence layer for the graph — is left empty or half-written, and the + * accumulated memory is unrecoverable. + * + * The fix writes to a temporary file in the same directory and then renames + * it over the target. rename(2) is atomic on POSIX filesystems: a reader + * either sees the complete old file or the complete new one, never a + * truncated intermediate state. + */ +describe('KnowledgeGraphManager persistence durability', () => { + let testDir: string; + let testFilePath: string; + + beforeEach(async () => { + testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-memory-atomic-')); + testFilePath = path.join(testDir, 'memory.jsonl'); + }); + + afterEach(async () => { + vi.restoreAllMocks(); + await fs.rm(testDir, { recursive: true, force: true }); + }); + + it('never truncates the live memory file in place', async () => { + const manager = new KnowledgeGraphManager(testFilePath); + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] }, + ]); + + // Any write that targets the live file directly is destructive: it + // truncates committed data before the replacement is durable. + const writeFileSpy = vi.spyOn(fs, 'writeFile'); + const openSpy = vi.spyOn(fs, 'open'); + + await manager.createEntities([ + { name: 'Bob', entityType: 'person', observations: ['likes programming'] }, + ]); + + const destructiveTargets = [ + ...writeFileSpy.mock.calls.map(call => call[0]), + ...openSpy.mock.calls.map(call => call[0]), + ].filter(target => target === testFilePath); + + expect(destructiveTargets).toEqual([]); + }); + + it('leaves the committed graph intact when the write fails midway', async () => { + const manager = new KnowledgeGraphManager(testFilePath); + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] }, + ]); + + const before = await fs.readFile(testFilePath, 'utf-8'); + expect(before).toContain('Alice'); + + // Simulate the process being interrupted while persisting the next write. + vi.spyOn(fs, 'writeFile').mockImplementationOnce(async () => { + throw new Error('ENOSPC: simulated interruption'); + }); + + await expect( + manager.createEntities([ + { name: 'Bob', entityType: 'person', observations: ['likes programming'] }, + ]) + ).rejects.toThrow(); + + vi.restoreAllMocks(); + + // The previously committed graph must survive untouched. + expect(await fs.readFile(testFilePath, 'utf-8')).toBe(before); + + const graph = await new KnowledgeGraphManager(testFilePath).readGraph(); + expect(graph.entities.map(e => e.name)).toEqual(['Alice']); + }); + + it('does not leave temporary files behind after a successful write', async () => { + const manager = new KnowledgeGraphManager(testFilePath); + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] }, + ]); + + expect(await fs.readdir(testDir)).toEqual(['memory.jsonl']); + }); + + it('cleans up the temporary file when the write fails', async () => { + const manager = new KnowledgeGraphManager(testFilePath); + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] }, + ]); + + vi.spyOn(fs, 'rename').mockImplementationOnce(async () => { + throw new Error('EXDEV: simulated rename failure'); + }); + + await expect( + manager.createEntities([ + { name: 'Bob', entityType: 'person', observations: ['likes programming'] }, + ]) + ).rejects.toThrow(); + + vi.restoreAllMocks(); + expect(await fs.readdir(testDir)).toEqual(['memory.jsonl']); + }); + + it('still persists graph contents correctly across reloads', async () => { + const manager = new KnowledgeGraphManager(testFilePath); + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] }, + { name: 'Bob', entityType: 'person', observations: ['likes programming'] }, + ]); + await manager.createRelations([ + { from: 'Alice', to: 'Bob', relationType: 'knows' }, + ]); + + const reloaded = await new KnowledgeGraphManager(testFilePath).readGraph(); + expect(reloaded.entities.map(e => e.name).sort()).toEqual(['Alice', 'Bob']); + expect(reloaded.relations).toEqual([ + { from: 'Alice', to: 'Bob', relationType: 'knows' }, + ]); + }); +}); diff --git a/src/memory/index.ts b/src/memory/index.ts index 9865c53..b8704a9 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -6,6 +6,7 @@ import { SubscribeRequestSchema, UnsubscribeRequestSchema } from "@modelcontextp import { z } from "zod"; import { promises as fs } from 'fs'; import path from 'path'; +import { randomBytes } from 'crypto'; import { fileURLToPath } from 'url'; // Define memory file path using environment variable with fallback @@ -114,7 +115,29 @@ export class KnowledgeGraphManager { relationType: r.relationType })), ]; - await fs.writeFile(this.memoryFilePath, lines.join("\n")); + + // Write to a temporary file in the same directory, then rename it over + // the target. fs.writeFile would truncate the memory file before writing, + // so an interruption (SIGKILL, container stop, OOM, power loss) would + // leave the only copy of the graph truncated and unrecoverable. + // rename(2) is atomic on POSIX filesystems: readers see either the + // complete old file or the complete new one, never a partial state. + // The temp file is kept in the same directory so the rename stays on one + // filesystem — renaming across mount points fails with EXDEV. + const directory = path.dirname(this.memoryFilePath); + const tempFilePath = path.join( + directory, + `${path.basename(this.memoryFilePath)}.${randomBytes(16).toString('hex')}.tmp` + ); + + try { + await fs.writeFile(tempFilePath, lines.join("\n")); + await fs.rename(tempFilePath, this.memoryFilePath); + } catch (error) { + // Never leave a stray temp file behind on failure. + await fs.unlink(tempFilePath).catch(() => {}); + throw error; + } } async createEntities(entities: Entity[]): Promise { From cf05b3769492ff94fbb25dad1a16c7703cd718c8 Mon Sep 17 00:00:00 2001 From: KXH Date: Sat, 29 Aug 2026 01:01:08 +0800 Subject: [PATCH 20/40] fix(filesystem): resolve Unicode-equivalent paths (#4638) Signed-off-by: KXH --- .../__tests__/unicode-paths.test.ts | 49 +++++++++++++++++ src/filesystem/lib.ts | 53 ++++++++++++++++--- 2 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 src/filesystem/__tests__/unicode-paths.test.ts diff --git a/src/filesystem/__tests__/unicode-paths.test.ts b/src/filesystem/__tests__/unicode-paths.test.ts new file mode 100644 index 0000000..2d28ac2 --- /dev/null +++ b/src/filesystem/__tests__/unicode-paths.test.ts @@ -0,0 +1,49 @@ +import fs from 'fs/promises'; +import os from 'os'; +import path from 'path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { setAllowedDirectories, validatePath } from '../lib.js'; + +describe('Unicode-equivalent filesystem paths', () => { + let testDirectory: string; + + beforeEach(async () => { + testDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-unicode-paths-')); + setAllowedDirectories([testDirectory]); + }); + + afterEach(async () => { + setAllowedDirectories([]); + await fs.rm(testDirectory, { recursive: true, force: true }); + }); + + it('resolves an existing decomposed path from a composed request', async () => { + const onDiskDirectory = 'de\u0301marche'; + const onDiskFile = 're\u0301sume\u0301.txt'; + await fs.mkdir(path.join(testDirectory, onDiskDirectory)); + await fs.writeFile(path.join(testDirectory, onDiskDirectory, onDiskFile), 'content'); + + const resolved = await validatePath(path.join(testDirectory, 'd\u00e9marche', 'r\u00e9sum\u00e9.txt')); + + expect(resolved).toBe(await fs.realpath(path.join(testDirectory, onDiskDirectory, onDiskFile))); + }); + + it('preserves a new basename after resolving a Unicode-equivalent parent', async () => { + const onDiskDirectory = 'de\u0301marche'; + await fs.mkdir(path.join(testDirectory, onDiskDirectory)); + + const resolved = await validatePath(path.join(testDirectory, 'd\u00e9marche', 'new.txt')); + + expect(resolved).toBe(path.join(await fs.realpath(path.join(testDirectory, onDiskDirectory)), 'new.txt')); + }); + + it('rejects ambiguous canonically equivalent entries', async () => { + const composed = 'caf\u00e9'; + const decomposed = 'cafe\u0301'; + await fs.mkdir(path.join(testDirectory, composed)); + await fs.mkdir(path.join(testDirectory, decomposed)); + + await expect(validatePath(path.join(testDirectory, 'cafe\u0341', 'file.txt'))) + .rejects.toThrow('Ambiguous Unicode path component'); + }); +}); diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index 3d0b49e..f1164c3 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -96,6 +96,46 @@ function resolveRelativePathAgainstAllowedDirectories(relativePath: string): str } // Security & Validation Functions +async function resolveUnicodeEquivalentPath(absolutePath: string): Promise { + const allowedDirectory = [...allowedDirectories] + .sort((left, right) => right.length - left.length) + .find(directory => isPathWithinAllowedDirectories(normalizePath(absolutePath), [directory])); + + if (!allowedDirectory) { + return absolutePath; + } + + let currentPath = await fs.realpath(allowedDirectory); + const relativeParts = path.relative(allowedDirectory, absolutePath).split(path.sep).filter(Boolean); + + for (let index = 0; index < relativeParts.length; index++) { + const requestedPart = relativeParts[index]; + const entries = (await fs.readdir(currentPath)) ?? []; + const exactMatch = entries.find(entry => entry === requestedPart); + const equivalentMatches = exactMatch + ? [exactMatch] + : entries.filter(entry => entry.normalize('NFC') === requestedPart.normalize('NFC')); + + if (equivalentMatches.length > 1) { + throw new Error(`Ambiguous Unicode path component: ${requestedPart}`); + } + + if (equivalentMatches.length === 0) { + if (index === relativeParts.length - 1) { + return path.join(currentPath, requestedPart); + } + throw new Error(`Parent directory does not exist: ${path.join(currentPath, requestedPart)}`); + } + + currentPath = await fs.realpath(path.join(currentPath, equivalentMatches[0])); + if (!isPathWithinAllowedDirectories(normalizePath(currentPath), allowedDirectories)) { + throw new Error(`Access denied - symlink target outside allowed directories: ${currentPath} not in ${allowedDirectories.join(', ')}`); + } + } + + return currentPath; +} + export async function validatePath(requestedPath: string): Promise { const expandedPath = expandHome(requestedPath); const absolute = path.isAbsolute(expandedPath) @@ -123,16 +163,13 @@ export async function validatePath(requestedPath: string): Promise { // Security: For new files that don't exist yet, verify parent directory // This ensures we can't create files in unauthorized locations if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - const parentDir = path.dirname(absolute); try { - const realParentPath = await fs.realpath(parentDir); - const normalizedParent = normalizePath(realParentPath); - if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirectories)) { - throw new Error(`Access denied - parent directory outside allowed directories: ${realParentPath} not in ${allowedDirectories.join(', ')}`); + return await resolveUnicodeEquivalentPath(absolute); + } catch (resolutionError) { + if ((resolutionError as NodeJS.ErrnoException).code === 'ENOENT') { + throw new Error(`Parent directory does not exist: ${path.dirname(absolute)}`); } - return absolute; - } catch { - throw new Error(`Parent directory does not exist: ${parentDir}`); + throw resolutionError; } } throw error; From 11845d51cbb42cba9695566f9134f5bd43d1216a Mon Sep 17 00:00:00 2001 From: Excelius <57819425+Excelius-Wang@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:01:16 +0800 Subject: [PATCH 21/40] fix(filesystem): preserve UTF-8 across head and tail chunks (#4667) --- src/filesystem/__tests__/lib.test.ts | 48 ++++++++++++++++++++++++++++ src/filesystem/lib.ts | 44 ++++++++++--------------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index a3db269..5c19e54 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -27,6 +27,23 @@ import { vi.mock('fs/promises'); const mockFs = fs as any; +function createMockFileHandle(content: Buffer) { + return { + read: vi.fn( + async (buffer: Buffer, offset: number, length: number, position: number) => { + const bytesRead = content.copy( + buffer, + offset, + position, + Math.min(position + length, content.length), + ); + return { bytesRead, buffer }; + }, + ), + close: vi.fn().mockResolvedValue(undefined), + }; +} + describe('Lib Functions', () => { beforeEach(() => { vi.clearAllMocks(); @@ -722,6 +739,23 @@ describe('Lib Functions', () => { expect(mockFileHandle.close).toHaveBeenCalled(); }); + it('preserves UTF-8 characters split across chunk boundaries', async () => { + const content = Buffer.concat([ + Buffer.from('discard\n'), + Buffer.from('界'), + Buffer.alloc(1017, 'a'), + Buffer.from('\nlast'), + ]); + const mockFileHandle = createMockFileHandle(content); + + mockFs.stat.mockResolvedValue({ size: content.length } as any); + mockFs.open.mockResolvedValue(mockFileHandle); + + const result = await tailFile('/test/file.txt', 2); + + expect(result).toBe(`界${'a'.repeat(1017)}\nlast`); + }); + it('handles read errors gracefully', async () => { mockFs.stat.mockResolvedValue({ size: 100 } as any); @@ -778,6 +812,20 @@ describe('Lib Functions', () => { expect(mockFileHandle.close).toHaveBeenCalled(); }); + it('preserves UTF-8 characters split across chunk boundaries', async () => { + const content = Buffer.concat([ + Buffer.alloc(1023, 'a'), + Buffer.from('界\nsecond'), + ]); + const mockFileHandle = createMockFileHandle(content); + + mockFs.open.mockResolvedValue(mockFileHandle); + + const result = await headFile('/test/file.txt', 1); + + expect(result).toBe(`${'a'.repeat(1023)}界`); + }); + it('handles files with leftover content', async () => { const mockFileHandle = { read: vi.fn(), diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index f1164c3..d01e35f 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -2,6 +2,7 @@ import fs from "fs/promises"; import path from "path"; import os from 'os'; import { randomBytes } from 'crypto'; +import { StringDecoder } from 'string_decoder'; import { diffLines, createTwoFilesPatch } from 'diff'; import { minimatch } from 'minimatch'; import { normalizePath, expandHome } from './path-utils.js'; @@ -364,42 +365,28 @@ export async function tailFile(filePath: string, numLines: number): Promise 0 && linesFound < numLines) { + while (position > 0 && newlinesFound < numLines) { const size = Math.min(CHUNK_SIZE, position); position -= size; const { bytesRead } = await fileHandle.read(chunk, 0, size, position); if (!bytesRead) break; - - // Get the chunk as a string and prepend any remaining text from previous iteration - const readData = chunk.slice(0, bytesRead).toString('utf-8'); - const chunkText = readData + remainingText; - - // Split by newlines and count - const chunkLines = normalizeLineEndings(chunkText).split('\n'); - - // If this isn't the end of the file, the first line is likely incomplete - // Save it to prepend to the next chunk - if (position > 0) { - remainingText = chunkLines[0]; - chunkLines.shift(); // Remove the first (incomplete) line - } - - // Add lines to our result (up to the number we need) - for (let i = chunkLines.length - 1; i >= 0 && linesFound < numLines; i--) { - lines.unshift(chunkLines[i]); - linesFound++; + + const readData = Buffer.from(chunk.subarray(0, bytesRead)); + chunks.unshift(readData); + for (const byte of readData) { + if (byte === 0x0a) newlinesFound++; } } - - return lines.join('\n'); + + const text = normalizeLineEndings(Buffer.concat(chunks).toString('utf-8')); + return text.split('\n').slice(-numLines).join('\n'); } finally { await fileHandle.close(); } @@ -413,13 +400,14 @@ export async function headFile(filePath: string, numLines: number): Promise 0 && lines.length < numLines) { From 40a8de87ed31a307c2a85cae21a78da9b7c1caf8 Mon Sep 17 00:00:00 2001 From: karl4c Date: Fri, 28 Aug 2026 13:01:24 -0400 Subject: [PATCH 22/40] fix(sequentialthinking): read server version from package.json (#4576) * fix(sequentialthinking): read server version from package.json Replace the hardcoded 0.2.0 so initialize serverInfo tracks the package version (0.6.2 in-tree; CalVer after release), matching the approach used for memory server version sync. Skip the dist-layout assertion when dist/ is absent so CI's test-before-build job (and local runs without prepare) still pass. * test(sequentialthinking): smoke stdio initialize serverInfo.version Spawn the built server over stdio and assert getServerVersion() matches package.json (and is not the old hardcoded 0.2.0). Skip when dist/ is absent so test-before-build CI still passes. --- .../__tests__/server-version.test.ts | 57 +++++++++++++++++++ src/sequentialthinking/index.ts | 3 +- src/sequentialthinking/version.ts | 33 +++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/sequentialthinking/__tests__/server-version.test.ts create mode 100644 src/sequentialthinking/version.ts diff --git a/src/sequentialthinking/__tests__/server-version.test.ts b/src/sequentialthinking/__tests__/server-version.test.ts new file mode 100644 index 0000000..c7d7f5c --- /dev/null +++ b/src/sequentialthinking/__tests__/server-version.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { createRequire } from 'node:module'; +import { existsSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { resolvePackageVersion, SERVER_VERSION } from '../version.js'; + +const packageJson = createRequire(import.meta.url)('../package.json') as { version: string }; +const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +const distVersionPath = path.join(packageRoot, 'dist', 'version.js'); +const distIndexPath = path.join(packageRoot, 'dist', 'index.js'); + +describe('server version', () => { + it('uses package.json version instead of a hardcoded string', () => { + expect(SERVER_VERSION).toBe(packageJson.version); + expect(resolvePackageVersion()).toBe(packageJson.version); + expect(SERVER_VERSION).not.toBe('0.2.0'); + }); + + // CI runs `npm test` before the dedicated build job. `npm ci` usually + // materializes dist/ via prepare, but that is not guaranteed (e.g. local + // `rm -rf dist && npm test`, or install with --ignore-scripts). + it.skipIf(!existsSync(distVersionPath))( + 'resolves package.json from the dist layout after build', + async () => { + const distModule = (await import(pathToFileURL(distVersionPath).href)) as { + SERVER_VERSION: string; + }; + expect(distModule.SERVER_VERSION).toBe(packageJson.version); + }, + ); + + it.skipIf(!existsSync(distIndexPath))( + 'stdio initialize reports package.json version in serverInfo', + async () => { + const transport = new StdioClientTransport({ + command: process.execPath, + args: [distIndexPath], + cwd: packageRoot, + stderr: 'pipe', + }); + const client = new Client({ name: 'version-smoke', version: '0.0.0' }); + + try { + await client.connect(transport); + const serverInfo = client.getServerVersion(); + expect(serverInfo?.name).toBe('sequential-thinking-server'); + expect(serverInfo?.version).toBe(packageJson.version); + expect(serverInfo?.version).not.toBe('0.2.0'); + } finally { + await client.close(); + } + }, + ); +}); diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index 217845b..54c42df 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -4,6 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { SequentialThinkingServer } from './lib.js'; +import { SERVER_VERSION } from './version.js'; /** Safe boolean coercion that correctly handles string "false" */ const coercedBoolean = z.preprocess((val) => { @@ -17,7 +18,7 @@ const coercedBoolean = z.preprocess((val) => { const server = new McpServer({ name: "sequential-thinking-server", - version: "0.2.0", + version: SERVER_VERSION, }); const thinkingServer = new SequentialThinkingServer(); diff --git a/src/sequentialthinking/version.ts b/src/sequentialthinking/version.ts new file mode 100644 index 0000000..1ab7d8f --- /dev/null +++ b/src/sequentialthinking/version.ts @@ -0,0 +1,33 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * Resolve this package's version from package.json. + * + * Works both from source (`src/sequentialthinking/`) and from the published + * layout (`dist/`), where package.json lives one directory up. + */ +export function resolvePackageVersion(): string { + const require = createRequire(import.meta.url); + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const candidates = [ + path.join(moduleDir, 'package.json'), + path.join(moduleDir, '..', 'package.json'), + ]; + + for (const candidate of candidates) { + try { + const pkg = require(candidate) as { version?: string }; + if (pkg.version) { + return pkg.version; + } + } catch { + // Try the next candidate when running from dist/ or source. + } + } + + throw new Error('Could not locate package.json for server version'); +} + +export const SERVER_VERSION = resolvePackageVersion(); From d6402efee6cc6446b23adbcb63051f9dc8612be5 Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Fri, 28 Aug 2026 11:57:34 -0600 Subject: [PATCH 23/40] fix(sequentialthinking): restore nextThoughtNeeded in the advertised inputSchema required array (#4652) * fix(sequentialthinking): restore nextThoughtNeeded in the advertised inputSchema required array commit 1cdf806d (#3533) wrapped nextThoughtNeeded in a z.preprocess-based coercedBoolean to fix a real footgun (string "false" coercing to true). zod's toJSONSchema(..., { io: "input" }) treats a z.preprocess()-wrapped field's input type as unknown, so it silently drops that field from the emitted required array, even though it carries no .optional(). A client that builds its call arguments from the advertised schema then omits nextThoughtNeeded and gets a -32602 Invalid params from the runtime validator, which still requires it. Rebuild coercedBoolean as a transform on an explicit z.union([z.boolean(), z.string()]) instead of z.preprocess. The union gives toJSONSchema a concrete input type to report, so nextThoughtNeeded stays in required, while parse behavior (including the case-insensitive string coercion #3533 added) is unchanged. Fixes #4651 * test(sequentialthinking): pin nextThoughtNeeded in required and string coercion Runs against the built server so it checks the schema the SDK emits. Skips when dist/ is absent, matching server-version.test.ts. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018Wo28CHPXyM3DHoLWvKnCK --------- Co-authored-by: olaservo Co-authored-by: Claude Fable 5 --- .../__tests__/input-schema.test.ts | 66 +++++++++++++++++++ src/sequentialthinking/index.ts | 16 ++--- 2 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 src/sequentialthinking/__tests__/input-schema.test.ts diff --git a/src/sequentialthinking/__tests__/input-schema.test.ts b/src/sequentialthinking/__tests__/input-schema.test.ts new file mode 100644 index 0000000..4ff7be6 --- /dev/null +++ b/src/sequentialthinking/__tests__/input-schema.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { existsSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +const distIndexPath = path.join(packageRoot, 'dist', 'index.js'); + +// Regression coverage for #4651: nextThoughtNeeded must stay in the advertised +// `required` array, and string coercion must keep accepting "True"/"FALSE" +// while rejecting anything else. Runs against the built server so it checks +// the schema the SDK actually emits, not the zod object. +describe.skipIf(!existsSync(distIndexPath))('sequentialthinking input schema', () => { + let client: Client; + + beforeAll(async () => { + const transport = new StdioClientTransport({ + command: process.execPath, + args: [distIndexPath], + cwd: packageRoot, + stderr: 'pipe', + }); + client = new Client({ name: 'input-schema-test', version: '0.0.0' }); + await client.connect(transport); + }); + + afterAll(async () => { + await client?.close(); + }); + + it('advertises nextThoughtNeeded as required', async () => { + const { tools } = await client.listTools(); + const tool = tools.find(t => t.name === 'sequentialthinking'); + expect(tool).toBeDefined(); + expect(tool!.inputSchema.required).toEqual( + expect.arrayContaining(['thought', 'nextThoughtNeeded', 'thoughtNumber', 'totalThoughts']) + ); + }); + + it('rejects a call that omits nextThoughtNeeded', async () => { + const result = await client.callTool({ + name: 'sequentialthinking', + arguments: { thought: 't', thoughtNumber: 1, totalThoughts: 1 }, + }); + expect(result.isError).toBe(true); + expect(JSON.stringify(result.content)).toMatch(/nextThoughtNeeded/); + }); + + it.each(['True', 'FALSE', 'true', 'false'])('accepts the string %s', async (value) => { + const result = await client.callTool({ + name: 'sequentialthinking', + arguments: { thought: 't', nextThoughtNeeded: value, thoughtNumber: 1, totalThoughts: 1 }, + }); + expect(result.isError).toBeFalsy(); + }); + + it.each(['yes', '', '1'])('rejects the string %j', async (value) => { + const result = await client.callTool({ + name: 'sequentialthinking', + arguments: { thought: 't', nextThoughtNeeded: value, thoughtNumber: 1, totalThoughts: 1 }, + }); + expect(result.isError).toBe(true); + }); +}); diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index 54c42df..1ae09d1 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -6,15 +6,15 @@ import { z } from "zod"; import { SequentialThinkingServer } from './lib.js'; import { SERVER_VERSION } from './version.js'; -/** Safe boolean coercion that correctly handles string "false" */ -const coercedBoolean = z.preprocess((val) => { +/** Safe boolean coercion that correctly handles string "false". A union+transform, + * not z.preprocess (whose input type is `unknown`), so toJSONSchema keeps this required. */ +const coercedBoolean = z.union([z.boolean(), z.string()]).transform((val, ctx) => { if (typeof val === "boolean") return val; - if (typeof val === "string") { - if (val.toLowerCase() === "true") return true; - if (val.toLowerCase() === "false") return false; - } - return val; -}, z.boolean()); + if (val.toLowerCase() === "true") return true; + if (val.toLowerCase() === "false") return false; + ctx.addIssue({ code: "custom", message: `Expected boolean or "true"/"false" string, received "${val}"` }); + return z.NEVER; +}); const server = new McpServer({ name: "sequential-thinking-server", From cda92bdaacd558192fedf1a60d2bb27510792388 Mon Sep 17 00:00:00 2001 From: Riley Date: Fri, 28 Aug 2026 18:58:19 +0100 Subject: [PATCH 24/40] fix(filesystem): create_directory doesn't create parent directories (#4631) validatePath()'s ENOENT fallback checked only the immediate parent directory before allowing a new path through. Creating a path with more than one missing level (e.g. a/b/c when none of a, b, c exist) made the immediate parent check fail with "Parent directory does not exist", even though fs.mkdir was already called with recursive: true one level up in the create_directory handler and an existing allowed ancestor was available further up the tree. Walk up through missing ancestors until an existing one is found (or the filesystem root is reached), verifying that ancestor still resolves inside an allowed directory via realpath. The top-level lexical containment check already ran against the full target path, so this only extends how far the existence/symlink check climbs. Fixes #4629 Co-authored-by: Riley <313779891+rileybuilds@users.noreply.github.com> Co-authored-by: olaservo Co-authored-by: Claude Fable 5 --- src/filesystem/__tests__/lib.test.ts | 39 +++++++++++++------ .../__tests__/nested-parents.test.ts | 35 +++++++++++++++++ src/filesystem/lib.ts | 8 ++-- 3 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 src/filesystem/__tests__/nested-parents.test.ts diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index 5c19e54..eeee46b 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -208,19 +208,34 @@ describe('Lib Functions', () => { expect(result).toBe(path.resolve(newFilePath)); }); - it('rejects when parent directory does not exist', async () => { + it('walks past multiple missing ancestors to an existing allowed directory', async () => { + // e.g. create_directory('/home/user/nonexistent/nested/newfile.txt') when + // neither 'nonexistent' nor 'nonexistent/nested' exist yet, but '/home/user' + // (an allowed directory) does. Regression test for #4629. + const newFilePath = process.platform === 'win32' ? 'C:\\Users\\test\\nonexistent\\nested\\newfile.txt' : '/home/user/nonexistent/nested/newfile.txt'; + + const enoentError = new Error('ENOENT') as NodeJS.ErrnoException; + enoentError.code = 'ENOENT'; + + // The path itself is missing. The resolver then realpaths the allowed + // directory ('/home/user'), which falls through to the beforeEach base + // implementation and echoes the path back as-is. readdir is mocked to + // undefined, so every component below it counts as missing. + mockFs.realpath.mockRejectedValueOnce(enoentError); + + const result = await validatePath(newFilePath); + expect(result).toBe(path.resolve(newFilePath)); + }); + + it('rejects when no ancestor directory exists at all', async () => { const newFilePath = process.platform === 'win32' ? 'C:\\Users\\test\\nonexistent\\newfile.txt' : '/home/user/nonexistent/newfile.txt'; - - // Create errors with the ENOENT code - const enoentError1 = new Error('ENOENT') as NodeJS.ErrnoException; - enoentError1.code = 'ENOENT'; - const enoentError2 = new Error('ENOENT') as NodeJS.ErrnoException; - enoentError2.code = 'ENOENT'; - - mockFs.realpath - .mockRejectedValueOnce(enoentError1) - .mockRejectedValueOnce(enoentError2); - + + const enoentError = new Error('ENOENT') as NodeJS.ErrnoException; + enoentError.code = 'ENOENT'; + + // Every ancestor, all the way up to the filesystem root, is missing. + mockFs.realpath.mockRejectedValue(enoentError); + await expect(validatePath(newFilePath)) .rejects.toThrow('Parent directory does not exist'); }); diff --git a/src/filesystem/__tests__/nested-parents.test.ts b/src/filesystem/__tests__/nested-parents.test.ts new file mode 100644 index 0000000..854dc44 --- /dev/null +++ b/src/filesystem/__tests__/nested-parents.test.ts @@ -0,0 +1,35 @@ +import fs from 'fs/promises'; +import os from 'os'; +import path from 'path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { setAllowedDirectories, validatePath } from '../lib.js'; + +// Regression coverage for #4629: validatePath must accept a path whose +// ancestors are missing several levels deep, so create_directory can mkdir -p. +describe('validatePath with multiple missing ancestors', () => { + let allowedDir: string; + let outsideDir: string; + + beforeEach(async () => { + allowedDir = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-nested-allowed-'))); + outsideDir = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-nested-outside-'))); + setAllowedDirectories([allowedDir]); + }); + + afterEach(async () => { + setAllowedDirectories([]); + await fs.rm(allowedDir, { recursive: true, force: true }); + await fs.rm(outsideDir, { recursive: true, force: true }); + }); + + it('returns the full path when several ancestors do not exist', async () => { + const requested = path.join(allowedDir, 'a', 'b', 'c'); + await expect(validatePath(requested)).resolves.toBe(requested); + }); + + it('rejects when the nearest existing ancestor is a symlink out of the allowed tree', async () => { + await fs.symlink(outsideDir, path.join(allowedDir, 'link'), 'junction'); + await expect(validatePath(path.join(allowedDir, 'link', 'a', 'b'))) + .rejects.toThrow('Access denied'); + }); +}); diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index d01e35f..f2371a9 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -122,10 +122,10 @@ async function resolveUnicodeEquivalentPath(absolutePath: string): Promise Date: Sun, 30 Aug 2026 15:07:02 -0500 Subject: [PATCH 25/40] fix: Containers - Bind Mounts - Security and Compatibility (Docker / Podman / SELinux) for Fetch, Git, and Time Servers (#2205) * Update Dockerfile for fetch Initial patch for podman and SELinux contexts. * Update Dockerfile for time podman and SELinux * Update Dockerfile * Also include app user / group (securely) such that the build succeeds * Container app user optional add (#1) * Update Dockerfile git * Update Dockerfile fetch * Update Dockerfile time * Update Dockerfile * Update Dockerfile * Update Dockerfile --------- Co-authored-by: Ola Hungerford --- src/fetch/Dockerfile | 7 +++++-- src/git/Dockerfile | 7 +++++-- src/time/Dockerfile | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/fetch/Dockerfile b/src/fetch/Dockerfile index e81610c..d2a4504 100644 --- a/src/fetch/Dockerfile +++ b/src/fetch/Dockerfile @@ -11,9 +11,8 @@ ENV UV_COMPILE_BYTECODE=1 ENV UV_LINK_MODE=copy # Install the project's dependencies using the lockfile and settings +COPY uv.lock pyproject.toml ./ RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project --no-dev --no-editable # Then, add the rest of the project source code and install it @@ -27,6 +26,10 @@ FROM python:3.12-slim-bookworm WORKDIR /app COPY --from=uv /root/.local /root/.local + +RUN if ! id -u app >/dev/null 2>&1; then \ + useradd -rUM -s /usr/sbin/nologin app; \ + fi COPY --from=uv --chown=app:app /app/.venv /app/.venv # Place executables in the environment at the front of the path diff --git a/src/git/Dockerfile b/src/git/Dockerfile index 6b53886..4af4161 100644 --- a/src/git/Dockerfile +++ b/src/git/Dockerfile @@ -11,9 +11,8 @@ ENV UV_COMPILE_BYTECODE=1 ENV UV_LINK_MODE=copy # Install the project's dependencies using the lockfile and settings +COPY uv.lock pyproject.toml ./ RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project --no-dev --no-editable # Then, add the rest of the project source code and install it @@ -30,6 +29,10 @@ RUN apt-get update && apt-get install -y git git-lfs && rm -rf /var/lib/apt/list WORKDIR /app COPY --from=uv /root/.local /root/.local + +RUN if ! id -u app >/dev/null 2>&1; then \ + useradd -rUM -s /usr/sbin/nologin app; \ + fi COPY --from=uv --chown=app:app /app/.venv /app/.venv # Place executables in the environment at the front of the path diff --git a/src/time/Dockerfile b/src/time/Dockerfile index ac5f752..c928732 100644 --- a/src/time/Dockerfile +++ b/src/time/Dockerfile @@ -11,9 +11,8 @@ ENV UV_COMPILE_BYTECODE=1 ENV UV_LINK_MODE=copy # Install the project's dependencies using the lockfile and settings +COPY uv.lock pyproject.toml ./ RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project --no-dev --no-editable # Then, add the rest of the project source code and install it @@ -27,6 +26,10 @@ FROM python:3.12-slim-bookworm WORKDIR /app COPY --from=uv /root/.local /root/.local + +RUN if ! id -u app >/dev/null 2>&1; then \ + useradd -rUM -s /usr/sbin/nologin app; \ + fi COPY --from=uv --chown=app:app /app/.venv /app/.venv # Place executables in the environment at the front of the path From a6cdbf4deac97aac5c9b76bd12d38eace78dac01 Mon Sep 17 00:00:00 2001 From: Yohanes Date: Mon, 31 Aug 2026 05:43:38 +0800 Subject: [PATCH 26/40] fix(filesystem): reject Windows paths on POSIX (#4704) Co-authored-by: Yohanes --- src/filesystem/__tests__/lib.test.ts | 7 +++++++ src/filesystem/lib.ts | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/filesystem/__tests__/lib.test.ts b/src/filesystem/__tests__/lib.test.ts index eeee46b..24eb18a 100644 --- a/src/filesystem/__tests__/lib.test.ts +++ b/src/filesystem/__tests__/lib.test.ts @@ -173,6 +173,13 @@ describe('Lib Functions', () => { describe('Security & Validation Functions', () => { describe('validatePath', () => { + it('rejects Windows drive paths on POSIX hosts', async () => { + if (process.platform === 'win32') return; + + await expect(validatePath('C:\\Users\\me\\notes\\file.md')) + .rejects.toThrow('Windows-style path received on a POSIX host'); + }); + // Use Windows-compatible paths for testing const allowedDirs = process.platform === 'win32' ? ['C:\\Users\\test', 'C:\\temp'] : ['/home/user', '/tmp']; diff --git a/src/filesystem/lib.ts b/src/filesystem/lib.ts index f2371a9..6cd2165 100644 --- a/src/filesystem/lib.ts +++ b/src/filesystem/lib.ts @@ -139,6 +139,12 @@ async function resolveUnicodeEquivalentPath(absolutePath: string): Promise { const expandedPath = expandHome(requestedPath); + // Do not silently reinterpret a Windows drive path as a relative POSIX path. + // This would create a literal filename such as `C:\\Users\\...` inside the + // allowed root and report success for the wrong location. + if (process.platform !== 'win32' && /^(?:[A-Za-z]:)(?:[\\/]|$)/.test(expandedPath)) { + throw new Error(`Access denied - Windows-style path received on a POSIX host: ${requestedPath}`); + } const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : resolveRelativePathAgainstAllowedDirectories(expandedPath); From 579c3903f30044eb702a599a74b3ae77588e722e Mon Sep 17 00:00:00 2001 From: Abhinav Prakash <60460989+AbhiPra24@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:52:36 +0530 Subject: [PATCH 27/40] docs(brave-search): point to official @brave/brave-search-mcp-server package (#4714) Fixes #4694 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fe5351a..9c388c8 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ These servers aim to demonstrate MCP features and the official SDKs. The following reference servers are now archived and can be found at [servers-archived](https://github.com/modelcontextprotocol/servers-archived). - **[AWS KB Retrieval](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/aws-kb-retrieval-server)** - Retrieval from AWS Knowledge Base using Bedrock Agent Runtime. -- **[Brave Search](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search)** - Web and local search using Brave's Search API. Has been replaced by the [official server](https://github.com/brave/brave-search-mcp-server). +- **[Brave Search](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/brave-search)** - Web and local search using Brave's Search API. Has been replaced by the [official server](https://github.com/brave/brave-search-mcp-server) ([`@brave/brave-search-mcp-server`](https://www.npmjs.com/package/@brave/brave-search-mcp-server)). - **[EverArt](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/everart)** - AI image generation using various models. - **[GitHub](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/github)** - Repository management, file operations, and GitHub API integration. - **[GitLab](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/gitlab)** - GitLab API, enabling project management. From 3aa19f557436298c0cc514fcab5e3b5eaacc1fc3 Mon Sep 17 00:00:00 2001 From: Abhinav Prakash <60460989+AbhiPra24@users.noreply.github.com> Date: Wed, 2 Sep 2026 07:55:31 +0530 Subject: [PATCH 28/40] fix(git): unify git_log output schema and remove raise_exceptions from server.run (#4658) --- src/git/src/mcp_server_git/server.py | 61 ++++++++------------- src/git/tests/test_server.py | 80 ++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 39 deletions(-) diff --git a/src/git/src/mcp_server_git/server.py b/src/git/src/mcp_server_git/server.py index 84188d8..b94af84 100644 --- a/src/git/src/mcp_server_git/server.py +++ b/src/git/src/mcp_server_git/server.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Sequence, Optional +from typing import Any, Optional, Sequence from mcp.server import Server from mcp.server.session import ServerSession from mcp.server.stdio import stdio_server @@ -157,45 +157,28 @@ def git_reset(repo: git.Repo) -> str: return "All staged changes reset" def git_log(repo: git.Repo, max_count: int = 10, start_timestamp: Optional[str] = None, end_timestamp: Optional[str] = None) -> list[str]: - if start_timestamp or end_timestamp: - # Defense in depth: reject timestamps starting with '-' to prevent flag injection - if start_timestamp and start_timestamp.startswith("-"): - raise ValueError(f"Invalid start_timestamp: '{start_timestamp}' - cannot start with '-'") - if end_timestamp and end_timestamp.startswith("-"): - raise ValueError(f"Invalid end_timestamp: '{end_timestamp}' - cannot start with '-'") - # Use git log command with date filtering - args = [] - if start_timestamp: - args.extend(['--since', start_timestamp]) - if end_timestamp: - args.extend(['--until', end_timestamp]) - args.extend(['--format=%H%n%an%n%ad%n%s%n']) + # Defense in depth: reject timestamps starting with '-' to prevent flag injection + if start_timestamp and start_timestamp.startswith("-"): + raise ValueError(f"Invalid start_timestamp: '{start_timestamp}' - cannot start with '-'") + if end_timestamp and end_timestamp.startswith("-"): + raise ValueError(f"Invalid end_timestamp: '{end_timestamp}' - cannot start with '-'") - log_output = repo.git.log(*args).split('\n') + kwargs: dict[str, Any] = {"max_count": max_count} + if start_timestamp: + kwargs["since"] = start_timestamp + if end_timestamp: + kwargs["until"] = end_timestamp - log = [] - # Process commits in groups of 4 (hash, author, date, message) - for i in range(0, len(log_output), 4): - if i + 3 < len(log_output) and len(log) < max_count: - log.append( - f"Commit: {log_output[i]}\n" - f"Author: {log_output[i+1]}\n" - f"Date: {log_output[i+2]}\n" - f"Message: {log_output[i+3]}\n" - ) - return log - else: - # Use existing logic for simple log without date filtering - commits = list(repo.iter_commits(max_count=max_count)) - log = [] - for commit in commits: - log.append( - f"Commit: {commit.hexsha!r}\n" - f"Author: {commit.author!r}\n" - f"Date: {commit.authored_datetime}\n" - f"Message: {commit.message!r}\n" - ) - return log + commits = list(repo.iter_commits(**kwargs)) + log = [] + for commit in commits: + log.append( + f"Commit: {commit.hexsha}\n" + f"Author: {commit.author}\n" + f"Date: {commit.authored_datetime}\n" + f"Message: {commit.message}\n" + ) + return log def git_create_branch(repo: git.Repo, branch_name: str, base_branch: str | None = None) -> str: # Defense in depth: reject names starting with '-' to prevent flag injection @@ -599,4 +582,4 @@ async def serve(repository: Path | None) -> None: options = server.create_initialization_options() async with stdio_server() as (read_stream, write_stream): - await server.run(read_stream, write_stream, options, raise_exceptions=True) + await server.run(read_stream, write_stream, options) diff --git a/src/git/tests/test_server.py b/src/git/tests/test_server.py index 893195d..05d5931 100644 --- a/src/git/tests/test_server.py +++ b/src/git/tests/test_server.py @@ -16,8 +16,10 @@ from mcp_server_git.server import ( git_create_branch, git_show, validate_repo_path, + serve, ) import shutil +import unittest.mock as mock @pytest.fixture def test_repository(tmp_path: Path): @@ -508,3 +510,81 @@ def test_git_branch_rejects_contains_flag_injection(test_repository): with pytest.raises(BadName): git_branch(test_repository, "local", not_contains="--exec=evil") + + +def test_git_log_formatting_no_repr(test_repository): + """Test that git_log does not use !r formatting (no Python object repr or quotes).""" + file_path = Path(test_repository.working_dir) / "multiline.txt" + file_path.write_text("multiline test") + test_repository.index.add(["multiline.txt"]) + test_repository.index.commit("Subject line\n\nDetailed body line 1\nDetailed body line 2") + + result = git_log(test_repository, max_count=1) + entry = result[0] + + # Verify no python repr quotes around commit hash or message + assert "Commit: '" not in entry + assert 'Commit: "' not in entry + assert " Date: Tue, 1 Sep 2026 20:01:29 -0700 Subject: [PATCH 29/40] ci: run readme-pr-check on pull_request_target so fork PRs can be labeled (#4739) Observed on #4714 and #4726: addLabels returned 403 "Resource not accessible by integration" because pull_request gives fork PRs a read-only token. The job only calls the GitHub API and never checks out PR code, so pull_request_target is safe here. Claude-Session: https://claude.ai/code/session_01CK8vvB229h6orvkokG8YxU Co-authored-by: Claude Fable 5 --- .github/workflows/readme-pr-check.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/readme-pr-check.yml b/.github/workflows/readme-pr-check.yml index 66cb3a3..ec5a0a2 100644 --- a/.github/workflows/readme-pr-check.yml +++ b/.github/workflows/readme-pr-check.yml @@ -1,7 +1,10 @@ name: README PR Check on: - pull_request: + # pull_request_target, not pull_request: fork PRs get a read-only token under + # pull_request, so the label and comment calls fail with 403. This job only + # calls the API and never checks out PR code, so the write token is safe. + pull_request_target: types: [opened] paths: - 'README.md' @@ -10,7 +13,7 @@ on: jobs: check-readme-only: - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest permissions: contents: read From cce08de417857f25941503be903a86f970055c21 Mon Sep 17 00:00:00 2001 From: Yusufhan Sacak <67666222+JosephDoUrden@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:03:51 +0300 Subject: [PATCH 30/40] fix(memory): resolve Vitest false positive for expected rejection (#3893) Wrap the addObservations call in an async closure before asserting with .rejects.toThrow(). This prevents Vitest's global unhandled rejection handler from capturing the expected error before the assertion can intercept it, eliminating the false-positive "Unhandled Errors" warning that was failing CI. Fixes #3073 --- src/memory/__tests__/knowledge-graph.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index 2362424..628ca56 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -146,11 +146,11 @@ describe('KnowledgeGraphManager', () => { }); it('should throw error for non-existent entity', async () => { - await expect( - manager.addObservations([ + await expect(async () => { + await manager.addObservations([ { entityName: 'NonExistent', contents: ['some observation'] }, - ]) - ).rejects.toThrow('Entity with name NonExistent not found'); + ]); + }).rejects.toThrow('Entity with name NonExistent not found'); }); }); From 96c49c73647ba8904a6916f070f282691111db19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E5=B2=B3=E5=B3=B0?= <132282304+syf2211@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:04:28 -0700 Subject: [PATCH 31/40] fix(memory): read serverInfo.version from package.json (#4407) Fixes #4406. The memory server previously hardcoded serverInfo.version as "0.6.3", which no longer matched published calendar versions. Resolve package.json from both source and dist layouts so Docker and npm installs report the installed package version. Co-authored-by: syf2211 --- src/memory/__tests__/server-version.test.ts | 25 +++++++++++++++++++ src/memory/index.ts | 4 +-- src/memory/version.ts | 27 +++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/memory/__tests__/server-version.test.ts create mode 100644 src/memory/version.ts diff --git a/src/memory/__tests__/server-version.test.ts b/src/memory/__tests__/server-version.test.ts new file mode 100644 index 0000000..f9cf437 --- /dev/null +++ b/src/memory/__tests__/server-version.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { createRequire } from 'node:module'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { resolvePackageVersion, SERVER_VERSION } from '../version.js'; + +const packageJson = createRequire(import.meta.url)('../package.json') as { version: string }; + +describe('server version', () => { + it('uses package.json version for serverInfo', () => { + expect(SERVER_VERSION).toBe(packageJson.version); + expect(resolvePackageVersion()).toBe(packageJson.version); + }); + + it('resolves package.json from the dist layout', () => { + const distDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist'); + const distVersionPath = path.join(distDir, 'version.js'); + + expect(() => createRequire(distVersionPath)('./version.js')).not.toThrow(); + const distModule = createRequire(distVersionPath)('./version.js') as { + SERVER_VERSION: string; + }; + expect(distModule.SERVER_VERSION).toBe(packageJson.version); + }); +}); diff --git a/src/memory/index.ts b/src/memory/index.ts index b8704a9..d5b862c 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -8,6 +8,7 @@ import { promises as fs } from 'fs'; import path from 'path'; import { randomBytes } from 'crypto'; import { fileURLToPath } from 'url'; +import { SERVER_VERSION } from './version.js'; // Define memory file path using environment variable with fallback export const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.jsonl'); @@ -276,10 +277,9 @@ const RelationSchema = z.object({ relationType: z.string().describe("The type of the relation") }); -// The server instance and tools exposed to Claude const server = new McpServer({ name: "memory-server", - version: "0.6.3", + version: SERVER_VERSION, }); const RESOURCE_URI = "memory://knowledge-graph"; diff --git a/src/memory/version.ts b/src/memory/version.ts new file mode 100644 index 0000000..f1df958 --- /dev/null +++ b/src/memory/version.ts @@ -0,0 +1,27 @@ +import { createRequire } from 'node:module'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +export function resolvePackageVersion(): string { + const require = createRequire(import.meta.url); + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const candidates = [ + path.join(moduleDir, 'package.json'), + path.join(moduleDir, '..', 'package.json'), + ]; + + for (const candidate of candidates) { + try { + const pkg = require(candidate) as { version?: string }; + if (pkg.version) { + return pkg.version; + } + } catch { + // Try the next candidate when running from dist/ or source. + } + } + + throw new Error('Could not locate package.json for server version'); +} + +export const SERVER_VERSION = resolvePackageVersion(); From 48723c95dc54ffb743650f0d75484434c8c254c6 Mon Sep 17 00:00:00 2001 From: King Star Date: Wed, 2 Sep 2026 23:04:34 +0800 Subject: [PATCH 32/40] fix(memory): reject dangling relations (#4477) Signed-off-by: King Star --- src/memory/README.md | 1 + src/memory/__tests__/knowledge-graph.test.ts | 32 ++++++++++++++++++++ src/memory/index.ts | 11 +++++++ 3 files changed, 44 insertions(+) diff --git a/src/memory/README.md b/src/memory/README.md index 0f29423..18851ae 100644 --- a/src/memory/README.md +++ b/src/memory/README.md @@ -72,6 +72,7 @@ Example: - `to` (string): Target entity name - `relationType` (string): Relationship type in active voice - Skips duplicate relations + - Fails if either the source or target entity doesn't exist - **add_observations** - Add new observations to existing entities diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index 628ca56..61e823a 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -99,6 +99,38 @@ describe('KnowledgeGraphManager', () => { expect(graph.relations).toHaveLength(1); }); + it('should reject relations from non-existent entities', async () => { + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: [] }, + ]); + + await expect( + manager.createRelations([ + { from: 'Ghost', to: 'Alice', relationType: 'knows' }, + ]) + ).rejects.toThrow('Entity with name Ghost not found'); + + const graph = await manager.readGraph(); + expect(graph.relations).toHaveLength(0); + }); + + it('should reject relation batches that reference non-existent target entities', async () => { + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: [] }, + { name: 'Bob', entityType: 'person', observations: [] }, + ]); + + await expect( + manager.createRelations([ + { from: 'Alice', to: 'Bob', relationType: 'knows' }, + { from: 'Alice', to: 'Ghost', relationType: 'knows' }, + ]) + ).rejects.toThrow('Entity with name Ghost not found'); + + const graph = await manager.readGraph(); + expect(graph.relations).toHaveLength(0); + }); + it('should handle empty relation arrays', async () => { const newRelations = await manager.createRelations([]); expect(newRelations).toHaveLength(0); diff --git a/src/memory/index.ts b/src/memory/index.ts index d5b862c..3f1179d 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -151,6 +151,17 @@ export class KnowledgeGraphManager { async createRelations(relations: Relation[]): Promise { const graph = await this.loadGraph(); + const entityNames = new Set(graph.entities.map(e => e.name)); + + relations.forEach(r => { + if (!entityNames.has(r.from)) { + throw new Error(`Entity with name ${r.from} not found`); + } + if (!entityNames.has(r.to)) { + throw new Error(`Entity with name ${r.to} not found`); + } + }); + const newRelations = relations.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from && existingRelation.to === r.to && From ba167cef5bcc1244f95b4fe9a6fbe6d488b10e52 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:15:26 +0800 Subject: [PATCH 33/40] fix: declare zod runtime dependencies (#4289) Co-authored-by: olaservo --- package-lock.json | 9 ++++++--- src/filesystem/package.json | 3 ++- src/memory/package.json | 3 ++- src/sequentialthinking/package.json | 5 +++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1845571..632d340 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3866,7 +3866,8 @@ "@modelcontextprotocol/sdk": "^1.30.0", "diff": "^8.0.3", "glob": "^13.0.6", - "minimatch": "^10.0.1" + "minimatch": "^10.0.1", + "zod": "^4.0.0" }, "bin": { "mcp-server-filesystem": "dist/index.js" @@ -3886,7 +3887,8 @@ "version": "0.6.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.30.0" + "@modelcontextprotocol/sdk": "^1.30.0", + "zod": "^4.0.0" }, "bin": { "mcp-server-memory": "dist/index.js" @@ -3906,7 +3908,8 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", "chalk": "^5.3.0", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "zod": "^4.0.0" }, "bin": { "mcp-server-sequential-thinking": "dist/index.js" diff --git a/src/filesystem/package.json b/src/filesystem/package.json index 139c4f0..3c288a2 100644 --- a/src/filesystem/package.json +++ b/src/filesystem/package.json @@ -28,7 +28,8 @@ "@modelcontextprotocol/sdk": "^1.30.0", "diff": "^8.0.3", "glob": "^13.0.6", - "minimatch": "^10.0.1" + "minimatch": "^10.0.1", + "zod": "^4.0.0" }, "devDependencies": { "@types/diff": "^5.0.9", diff --git a/src/memory/package.json b/src/memory/package.json index 4fdcd3f..ea10979 100644 --- a/src/memory/package.json +++ b/src/memory/package.json @@ -25,7 +25,8 @@ "test": "vitest run --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.30.0" + "@modelcontextprotocol/sdk": "^1.30.0", + "zod": "^4.0.0" }, "devDependencies": { "@types/node": "^22", diff --git a/src/sequentialthinking/package.json b/src/sequentialthinking/package.json index 03fbb2b..a668a47 100644 --- a/src/sequentialthinking/package.json +++ b/src/sequentialthinking/package.json @@ -27,7 +27,8 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", "chalk": "^5.3.0", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "zod": "^4.0.0" }, "devDependencies": { "@types/node": "^22", @@ -37,4 +38,4 @@ "typescript": "^5.3.3", "vitest": "^4.1.8" } -} \ No newline at end of file +} From 242751e8ee46c8b87e1bd82cbc2fb0cbcf8ff9ea Mon Sep 17 00:00:00 2001 From: Nick Veenhof Date: Thu, 3 Sep 2026 03:15:35 +0200 Subject: [PATCH 34/40] fix(memory): add trailing newline to JSONL output (#3653) Co-authored-by: Nick Veenhof Co-authored-by: olaservo --- src/memory/__tests__/knowledge-graph.test.ts | 70 ++++++++++++++++++++ src/memory/index.ts | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index 61e823a..0a6fc8f 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -458,6 +458,76 @@ describe('KnowledgeGraphManager', () => { expect(JSON.parse(lines[1])).toHaveProperty('type', 'relation'); }); + it('should write a trailing newline to produce valid JSONL', async () => { + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: ['test'] }, + ]); + + const fileContent = await fs.readFile(testFilePath, 'utf-8'); + expect(fileContent.endsWith('\n')).toBe(true); + }); + + it('should produce a file where every line is individually valid JSON', async () => { + // This test catches the bug where saveGraph wrote lines.join("\n") + // without a trailing newline. When the file was later appended to + // (e.g. by a concurrent process or external tool), the last JSON + // object and the new first JSON object ended up on the same line, + // producing invalid JSONL like: + // {"type":"entity","name":"Alice"}{"type":"relation","from":"Alice",...} + // which fails with: "Unexpected non-whitespace character after JSON + // at position N" + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: ['test'] }, + { name: 'Bob', entityType: 'person', observations: [] }, + ]); + await manager.createRelations([ + { from: 'Alice', to: 'Bob', relationType: 'knows' }, + ]); + + const fileContent = await fs.readFile(testFilePath, 'utf-8'); + const allLines = fileContent.split('\n'); + + // Every non-empty line must be valid JSON on its own + for (const line of allLines) { + if (line.trim() === '') continue; + expect(() => JSON.parse(line)).not.toThrow(); + } + }); + + it('should not corrupt JSONL when content is appended to the file externally', async () => { + // Simulate the real-world corruption scenario: + // 1. saveGraph writes entities to the file + // 2. An external process appends a new JSON line to the file + // 3. loadGraph must still parse the file without errors + // + // Without a trailing newline on step 1, the appended content in + // step 2 lands on the same line as the last entity, producing + // invalid JSONL that breaks loadGraph. + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: ['original'] }, + ]); + + // Simulate an external append (e.g. another process, a script, or + // a crash-recovery replay). This is what triggers the bug: without + // a trailing newline, this JSON object concatenates onto line 1. + const externalLine = JSON.stringify({ + type: 'entity', + name: 'External', + entityType: 'person', + observations: ['appended externally'], + }); + await fs.appendFile(testFilePath, externalLine + '\n'); + + // A new manager instance forces a fresh loadGraph from disk + const manager2 = new KnowledgeGraphManager(testFilePath); + const graph = await manager2.readGraph(); + + // Both entities must load without a JSON parse error + expect(graph.entities).toHaveLength(2); + expect(graph.entities.map(e => e.name)).toContain('Alice'); + expect(graph.entities.map(e => e.name)).toContain('External'); + }); + it('should strip type field from entities when loading from file', async () => { // Create entities and relations (these get saved with type field) await manager.createEntities([ diff --git a/src/memory/index.ts b/src/memory/index.ts index 3f1179d..eb6560d 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -132,7 +132,7 @@ export class KnowledgeGraphManager { ); try { - await fs.writeFile(tempFilePath, lines.join("\n")); + await fs.writeFile(tempFilePath, lines.join("\n") + "\n"); await fs.rename(tempFilePath, this.memoryFilePath); } catch (error) { // Never leave a stray temp file behind on failure. From 649af5856e8fc4b8598ceb1e75cc31215a9e0e90 Mon Sep 17 00:00:00 2001 From: Amey Pawar <138877912+ameyypawar@users.noreply.github.com> Date: Thu, 3 Sep 2026 06:45:44 +0530 Subject: [PATCH 35/40] fix(memory): expand leading ~ in MEMORY_FILE_PATH to the home directory (#4447) * fix(memory): expand leading ~ in MEMORY_FILE_PATH to the home directory MCP clients pass MEMORY_FILE_PATH from JSON config, where no shell expands a leading "~". Because path.isAbsolute("~/memory.jsonl") is false, the value was joined onto the package directory, so the server persisted to a literal "~" folder inside the install instead of the user's intended location. Add an expandHome() helper that mirrors the one already used by the filesystem server (src/filesystem/path-utils.ts) and apply it in ensureMemoryFilePath() before the existing absolute/relative resolution. Absolute paths, relative paths, and a "~" not at the start are unaffected. Adds unit tests for expandHome plus an end-to-end test through ensureMemoryFilePath. Addresses #1600 * test(memory): use a literal ~/ path in the tilde expansion test path.join('~', ...) produces a backslash on Windows, which expandHome does not match, so the test failed there. --------- Co-authored-by: olaservo --- src/memory/__tests__/file-path.test.ts | 38 +++++++++++++++++++++++++- src/memory/index.ts | 23 +++++++++++++--- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/memory/__tests__/file-path.test.ts b/src/memory/__tests__/file-path.test.ts index d1a16e4..fe5fdeb 100644 --- a/src/memory/__tests__/file-path.test.ts +++ b/src/memory/__tests__/file-path.test.ts @@ -1,8 +1,9 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { promises as fs } from 'fs'; import path from 'path'; +import os from 'os'; import { fileURLToPath } from 'url'; -import { ensureMemoryFilePath, defaultMemoryPath } from '../index.js'; +import { ensureMemoryFilePath, defaultMemoryPath, expandHome } from '../index.js'; describe('ensureMemoryFilePath', () => { const testDir = path.dirname(fileURLToPath(import.meta.url)); @@ -72,6 +73,15 @@ describe('ensureMemoryFilePath', () => { expect(path.isAbsolute(result)).toBe(true); } }); + + it('should expand a leading "~/" to the home directory', async () => { + process.env.MEMORY_FILE_PATH = '~/custom-memory.jsonl'; + + const result = await ensureMemoryFilePath(); + + expect(result).toBe(path.join(os.homedir(), 'custom-memory.jsonl')); + expect(path.isAbsolute(result)).toBe(true); + }); }); describe('without MEMORY_FILE_PATH environment variable', () => { @@ -154,3 +164,29 @@ describe('ensureMemoryFilePath', () => { }); }); }); + +describe('expandHome', () => { + it('expands a bare "~" to the home directory', () => { + expect(expandHome('~')).toBe(os.homedir()); + }); + + it('expands a leading "~/" to the home directory', () => { + expect(expandHome('~/notes/memory.jsonl')).toBe( + path.join(os.homedir(), 'notes/memory.jsonl') + ); + }); + + it('leaves a "~" not followed by a separator unchanged', () => { + expect(expandHome('~backup.jsonl')).toBe('~backup.jsonl'); + }); + + it('leaves absolute paths unchanged', () => { + expect(expandHome('/var/data/memory.jsonl')).toBe('/var/data/memory.jsonl'); + }); + + it('leaves relative paths unchanged', () => { + expect(expandHome(path.join('data', 'memory.jsonl'))).toBe( + path.join('data', 'memory.jsonl') + ); + }); +}); diff --git a/src/memory/index.ts b/src/memory/index.ts index eb6560d..82fe384 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -6,6 +6,7 @@ import { SubscribeRequestSchema, UnsubscribeRequestSchema } from "@modelcontextp import { z } from "zod"; import { promises as fs } from 'fs'; import path from 'path'; +import os from 'os'; import { randomBytes } from 'crypto'; import { fileURLToPath } from 'url'; import { SERVER_VERSION } from './version.js'; @@ -13,13 +14,27 @@ import { SERVER_VERSION } from './version.js'; // Define memory file path using environment variable with fallback export const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.jsonl'); +// Expand a leading "~" to the user's home directory. MCP clients pass +// MEMORY_FILE_PATH from JSON config, where no shell performs tilde expansion, +// so an unexpanded "~" would otherwise be treated as a relative path and +// joined onto the package directory. Mirrors the helper of the same name in +// the filesystem server (src/filesystem/path-utils.ts). +export function expandHome(filepath: string): string { + if (filepath.startsWith('~/') || filepath === '~') { + return path.join(os.homedir(), filepath.slice(1)); + } + return filepath; +} + // Handle backward compatibility: migrate memory.json to memory.jsonl if needed export async function ensureMemoryFilePath(): Promise { if (process.env.MEMORY_FILE_PATH) { - // Custom path provided, use it as-is (with absolute path resolution) - return path.isAbsolute(process.env.MEMORY_FILE_PATH) - ? process.env.MEMORY_FILE_PATH - : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.MEMORY_FILE_PATH); + // Custom path provided. Expand a leading "~" first, then resolve relative + // paths against the package directory (absolute paths are used as-is). + const customPath = expandHome(process.env.MEMORY_FILE_PATH); + return path.isAbsolute(customPath) + ? customPath + : path.join(path.dirname(fileURLToPath(import.meta.url)), customPath); } // No custom path set, check for backward compatibility migration From abbbddb2e3cbf1d3c24814bdff874c3c55f652e8 Mon Sep 17 00:00:00 2001 From: Connor Moss <140426627+ConnorMoss02@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:34:53 -0400 Subject: [PATCH 36/40] fix(memory): stop reporting deletions that did not happen (#4738) delete_entities, delete_observations and delete_relations returned success: true with a hardcoded "deleted successfully" message regardless of what matched. An agent that mistypes an entity name is told its memory is clean while the data is still on disk, and nothing in the response contradicts that. addObservations throws for the same condition ten lines above, so the file already disagreed with itself. Staying quiet is deliberate and documented, so nothing throws and the output schema is unchanged. The three manager methods now return what they matched, and the handlers say so. A delete where everything is found returns the same message it always did. README updated: the three "Silent operation" bullets described the absence of an error, which is still true, but read as if the response said nothing either. --- src/memory/README.md | 6 +- src/memory/__tests__/delete-reporting.test.ts | 99 +++++++++++++++++++ src/memory/index.ts | 48 ++++++--- 3 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 src/memory/__tests__/delete-reporting.test.ts diff --git a/src/memory/README.md b/src/memory/README.md index 18851ae..de7be2c 100644 --- a/src/memory/README.md +++ b/src/memory/README.md @@ -87,7 +87,7 @@ Example: - Remove entities and their relations - Input: `entityNames` (string[]) - Cascading deletion of associated relations - - Silent operation if entity doesn't exist + - No error if an entity doesn't exist; the response reports which names were not found - **delete_observations** - Remove specific observations from entities @@ -95,7 +95,7 @@ Example: - Each object contains: - `entityName` (string): Target entity - `observations` (string[]): Observations to remove - - Silent operation if observation doesn't exist + - No error if an observation doesn't exist; the response reports how many were deleted - **delete_relations** - Remove specific relations from the graph @@ -104,7 +104,7 @@ Example: - `from` (string): Source entity name - `to` (string): Target entity name - `relationType` (string): Relationship type - - Silent operation if relation doesn't exist + - No error if a relation doesn't exist; the response reports how many were deleted - **read_graph** - Read the entire knowledge graph diff --git a/src/memory/__tests__/delete-reporting.test.ts b/src/memory/__tests__/delete-reporting.test.ts new file mode 100644 index 0000000..944c302 --- /dev/null +++ b/src/memory/__tests__/delete-reporting.test.ts @@ -0,0 +1,99 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { promises as fs } from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { KnowledgeGraphManager, Entity, Relation } from '../index.js'; + +/** + * The delete tools stay silent when a target is absent, which the README + * documents. What they must not do is report a deletion that did not happen: + * an agent that mistypes a name is told its memory is clean while the data is + * still on disk, and nothing in the response contradicts that. + */ +describe('delete reporting', () => { + let manager: KnowledgeGraphManager; + let testFilePath: string; + + const entities: Entity[] = [ + { name: 'Alice', entityType: 'person', observations: ['works at Acme Corp', 'likes tea'] }, + { name: 'Bob', entityType: 'person', observations: ['likes programming'] }, + ]; + const relations: Relation[] = [{ from: 'Alice', to: 'Bob', relationType: 'works_with' }]; + + beforeEach(async () => { + testFilePath = path.join( + path.dirname(fileURLToPath(import.meta.url)), + `test-delete-reporting-${Date.now()}-${Math.random().toString(16).slice(2)}.jsonl` + ); + manager = new KnowledgeGraphManager(testFilePath); + await manager.createEntities(entities); + await manager.createRelations(relations); + }); + + afterEach(async () => { + try { + await fs.unlink(testFilePath); + } catch { + // the file is gone already + } + }); + + describe('deleteEntities', () => { + it('reports which names matched and which did not', async () => { + const result = await manager.deleteEntities(['Alice', 'Alise']); + expect(result).toEqual({ deleted: ['Alice'], notFound: ['Alise'] }); + }); + + it('reports nothing deleted when no name matches', async () => { + const result = await manager.deleteEntities(['Nobody']); + expect(result).toEqual({ deleted: [], notFound: ['Nobody'] }); + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(2); + }); + + it('still deletes the entity and its relations', async () => { + await manager.deleteEntities(['Alice']); + + const graph = await manager.readGraph(); + expect(graph.entities.map(e => e.name)).toEqual(['Bob']); + expect(graph.relations).toHaveLength(0); + }); + }); + + describe('deleteObservations', () => { + it('counts only the observations that were present', async () => { + const result = await manager.deleteObservations([ + { entityName: 'Alice', observations: ['likes tea', 'never said this'] }, + ]); + expect(result).toEqual({ deletedCount: 1, missingEntities: [] }); + }); + + it('names an entity that does not exist', async () => { + const result = await manager.deleteObservations([ + { entityName: 'Carol', observations: ['anything'] }, + ]); + expect(result).toEqual({ deletedCount: 0, missingEntities: ['Carol'] }); + }); + }); + + describe('deleteRelations', () => { + it('counts only the relations that matched', async () => { + const result = await manager.deleteRelations([ + { from: 'Alice', to: 'Bob', relationType: 'works_with' }, + { from: 'Alice', to: 'Bob', relationType: 'never_existed' }, + ]); + expect(result).toEqual({ deletedCount: 1 }); + }); + + it('reports nothing deleted when the relation type is wrong', async () => { + const result = await manager.deleteRelations([ + { from: 'Alice', to: 'Bob', relationType: 'manages' }, + ]); + expect(result).toEqual({ deletedCount: 0 }); + + const graph = await manager.readGraph(); + expect(graph.relations).toHaveLength(1); + }); + }); +}); diff --git a/src/memory/index.ts b/src/memory/index.ts index 82fe384..95e0bc7 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -202,32 +202,45 @@ export class KnowledgeGraphManager { return results; } - async deleteEntities(entityNames: string[]): Promise { + async deleteEntities(entityNames: string[]): Promise<{ deleted: string[]; notFound: string[] }> { const graph = await this.loadGraph(); + const present = new Set(graph.entities.map(e => e.name)); + const deleted = entityNames.filter(name => present.has(name)); + const notFound = entityNames.filter(name => !present.has(name)); graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); await this.saveGraph(graph); + return { deleted, notFound }; } - async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise { + async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<{ deletedCount: number; missingEntities: string[] }> { const graph = await this.loadGraph(); + let deletedCount = 0; + const missingEntities: string[] = []; deletions.forEach(d => { const entity = graph.entities.find(e => e.name === d.entityName); if (entity) { + const before = entity.observations.length; entity.observations = entity.observations.filter(o => !d.observations.includes(o)); + deletedCount += before - entity.observations.length; + } else { + missingEntities.push(d.entityName); } }); await this.saveGraph(graph); + return { deletedCount, missingEntities }; } - async deleteRelations(relations: Relation[]): Promise { + async deleteRelations(relations: Relation[]): Promise<{ deletedCount: number }> { const graph = await this.loadGraph(); + const before = graph.relations.length; graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from && r.to === delRelation.to && r.relationType === delRelation.relationType )); await this.saveGraph(graph); + return { deletedCount: before - graph.relations.length }; } async readGraph(): Promise { @@ -436,11 +449,14 @@ server.registerTool( } }, async ({ entityNames }) => { - await knowledgeGraphManager.deleteEntities(entityNames); + const { deleted, notFound } = await knowledgeGraphManager.deleteEntities(entityNames); notifyGraphUpdated(); + const message = notFound.length === 0 + ? "Entities deleted successfully" + : `Deleted ${deleted.length} of ${entityNames.length} entities. Not found: ${notFound.join(", ")}`; return { - content: [{ type: "text" as const, text: "Entities deleted successfully" }], - structuredContent: { success: true, message: "Entities deleted successfully" } + content: [{ type: "text" as const, text: message }], + structuredContent: { success: true, message } }; } ); @@ -469,11 +485,16 @@ server.registerTool( } }, async ({ deletions }) => { - await knowledgeGraphManager.deleteObservations(deletions); + const { deletedCount, missingEntities } = await knowledgeGraphManager.deleteObservations(deletions); notifyGraphUpdated(); + const requested = deletions.reduce((total, d) => total + d.observations.length, 0); + const message = deletedCount === requested + ? "Observations deleted successfully" + : `Deleted ${deletedCount} of ${requested} observations.` + + (missingEntities.length ? ` Entities not found: ${missingEntities.join(", ")}` : ""); return { - content: [{ type: "text" as const, text: "Observations deleted successfully" }], - structuredContent: { success: true, message: "Observations deleted successfully" } + content: [{ type: "text" as const, text: message }], + structuredContent: { success: true, message } }; } ); @@ -499,11 +520,14 @@ server.registerTool( } }, async ({ relations }) => { - await knowledgeGraphManager.deleteRelations(relations); + const { deletedCount } = await knowledgeGraphManager.deleteRelations(relations); notifyGraphUpdated(); + const message = deletedCount === relations.length + ? "Relations deleted successfully" + : `Deleted ${deletedCount} of ${relations.length} relations. The rest matched nothing.`; return { - content: [{ type: "text" as const, text: "Relations deleted successfully" }], - structuredContent: { success: true, message: "Relations deleted successfully" } + content: [{ type: "text" as const, text: message }], + structuredContent: { success: true, message } }; } ); From c3d8e43d1f31a4ac99b7136eb7d8c7958ed9c376 Mon Sep 17 00:00:00 2001 From: Yiheng Zhao Date: Thu, 3 Sep 2026 09:34:58 +0800 Subject: [PATCH 37/40] fix(memory): validate knowledge graph entries when loading from disk (#4717) loadGraph() trusted the persisted memory file and pushed entities and relations without validating their fields. A corrupted or legacy entry (e.g. an entity missing entityType, or an observation that is not a string) would reach searchNodes and crash with "Cannot read properties of undefined (reading 'toLowerCase')". Validate each line against the existing EntitySchema/RelationSchema and skip malformed entries with a warning, so the in-memory graph only ever contains well-formed data. Malformed JSON lines are skipped as well. Fixes #2044 --- src/memory/__tests__/knowledge-graph.test.ts | 47 +++++++++++++++++ src/memory/index.ts | 55 ++++++++++++++------ 2 files changed, 86 insertions(+), 16 deletions(-) diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index 0a6fc8f..d1739f4 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -617,4 +617,51 @@ describe('KnowledgeGraphManager', () => { expect(result.relations[0]).not.toHaveProperty('type'); }); }); + + describe('loadGraph validation', () => { + it('skips corrupt entities instead of crashing search', async () => { + const lines = [ + JSON.stringify({ type: 'entity', name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] }), + JSON.stringify({ type: 'entity', name: 'Broken', observations: ['missing entityType'] }), + JSON.stringify({ type: 'entity', name: 'BadObs', entityType: 'person', observations: ['ok', null] }), + ]; + await fs.writeFile(testFilePath, lines.join('\n') + '\n'); + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(1); + expect(graph.entities[0].name).toBe('Alice'); + + // searchNodes must not throw even though the file contains corrupt entries + const result = await manager.searchNodes('Acme'); + expect(result.entities).toHaveLength(1); + expect(result.entities[0].name).toBe('Alice'); + }); + + it('skips corrupt relations', async () => { + const lines = [ + JSON.stringify({ type: 'entity', name: 'Alice', entityType: 'person', observations: [] }), + JSON.stringify({ type: 'relation', from: 'Alice', to: 'Bob' }), // missing relationType + JSON.stringify({ type: 'relation', from: 'Alice', to: 'Bob', relationType: 'knows' }), + ]; + await fs.writeFile(testFilePath, lines.join('\n') + '\n'); + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(1); + expect(graph.relations).toHaveLength(1); + expect(graph.relations[0].relationType).toBe('knows'); + }); + + it('skips malformed JSON lines', async () => { + const lines = [ + JSON.stringify({ type: 'entity', name: 'Alice', entityType: 'person', observations: [] }), + '{this is not valid json', + JSON.stringify({ type: 'entity', name: 'Bob', entityType: 'person', observations: [] }), + ]; + await fs.writeFile(testFilePath, lines.join('\n') + '\n'); + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(2); + expect(graph.entities.map(e => e.name)).toEqual(['Alice', 'Bob']); + }); + }); }); diff --git a/src/memory/index.ts b/src/memory/index.ts index 95e0bc7..c8d4987 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -90,24 +90,47 @@ export class KnowledgeGraphManager { try { const data = await fs.readFile(this.memoryFilePath, "utf-8"); const lines = data.split("\n").filter(line => line.trim() !== ""); - return lines.reduce((graph: KnowledgeGraph, line) => { - const item = JSON.parse(line); - if (item.type === "entity") { - graph.entities.push({ - name: item.name, - entityType: item.entityType, - observations: item.observations - }); + const graph: KnowledgeGraph = { entities: [], relations: [] }; + + for (const line of lines) { + let item: unknown; + try { + item = JSON.parse(line); + } catch { + console.error("Skipping malformed line in memory file"); + continue; } - if (item.type === "relation") { - graph.relations.push({ - from: item.from, - to: item.to, - relationType: item.relationType - }); + + if (typeof item !== "object" || item === null) { + console.error("Skipping non-object line in memory file"); + continue; } - return graph; - }, { entities: [], relations: [] }); + + const record = item as Record; + if (record.type === "entity") { + const parsed = EntitySchema.safeParse(item); + if (parsed.success) { + graph.entities.push(parsed.data); + } else { + console.error( + "Skipping invalid entity in memory file:", + parsed.error.issues.map(issue => `${issue.path.join(".")}: ${issue.message}`).join(", ") + ); + } + } else if (record.type === "relation") { + const parsed = RelationSchema.safeParse(item); + if (parsed.success) { + graph.relations.push(parsed.data); + } else { + console.error( + "Skipping invalid relation in memory file:", + parsed.error.issues.map(issue => `${issue.path.join(".")}: ${issue.message}`).join(", ") + ); + } + } + } + + return graph; } catch (error) { if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { return { entities: [], relations: [] }; From f41b666edf470795d3724b06899dbca2d102b4e2 Mon Sep 17 00:00:00 2001 From: fei Date: Thu, 3 Sep 2026 09:35:03 +0800 Subject: [PATCH 38/40] fix(memory): constrain search_nodes query length (#4662) The search_nodes tool accepted an unbounded query string (per modelcontextprotocol/servers#3537, official servers should constrain string parameters). An oversized query costs an O(graph) scan per call with no value. Cap it at 2048 chars via an exported SearchNodesQuerySchema, and add vitest coverage for the boundary (at-limit accepted, over-limit rejected, non-string still rejected). Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com> --- .../__tests__/search-nodes-schema.test.ts | 25 +++++++++++++++++++ src/memory/index.ts | 9 ++++++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/memory/__tests__/search-nodes-schema.test.ts diff --git a/src/memory/__tests__/search-nodes-schema.test.ts b/src/memory/__tests__/search-nodes-schema.test.ts new file mode 100644 index 0000000..c03f384 --- /dev/null +++ b/src/memory/__tests__/search-nodes-schema.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { SearchNodesQuerySchema, SEARCH_QUERY_MAX_LENGTH } from '../index.js'; + +describe('search_nodes input schema', () => { + it('should accept a normal query', () => { + expect(SearchNodesQuerySchema.safeParse('Alice').success).toBe(true); + expect(SearchNodesQuerySchema.safeParse('works at Acme Corp').success).toBe(true); + }); + + it('should accept a query at exactly the max length', () => { + const atLimit = 'a'.repeat(SEARCH_QUERY_MAX_LENGTH); + expect(SearchNodesQuerySchema.safeParse(atLimit).success).toBe(true); + }); + + it('should reject a query longer than the max length', () => { + const oversized = 'a'.repeat(SEARCH_QUERY_MAX_LENGTH + 1); + const result = SearchNodesQuerySchema.safeParse(oversized); + expect(result.success).toBe(false); + }); + + it('should still reject non-string input', () => { + expect(SearchNodesQuerySchema.safeParse(42).success).toBe(false); + expect(SearchNodesQuerySchema.safeParse(null).success).toBe(false); + }); +}); diff --git a/src/memory/index.ts b/src/memory/index.ts index c8d4987..7555e4a 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -582,6 +582,13 @@ server.registerTool( } ); +export const SEARCH_QUERY_MAX_LENGTH = 2048; + +export const SearchNodesQuerySchema = z + .string() + .max(SEARCH_QUERY_MAX_LENGTH) + .describe("The search query to match against entity names, types, and observation content"); + // Register search_nodes tool server.registerTool( "search_nodes", @@ -589,7 +596,7 @@ server.registerTool( title: "Search Nodes", description: "Search for nodes in the knowledge graph based on a query", inputSchema: { - query: z.string().describe("The search query to match against entity names, types, and observation content") + query: SearchNodesQuerySchema }, outputSchema: { entities: z.array(EntitySchema), From 1ec570c27d977a122a619da58ae3104f7fdf8be7 Mon Sep 17 00:00:00 2001 From: JSap0914 <116227558+JSap0914@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:38:22 +0900 Subject: [PATCH 39/40] fix(memory): skip duplicate entities and relations within a single batch (#4383) create_entities and create_relations only de-duplicated against the existing graph, so passing the same entity name (or identical relation) twice in one call persisted duplicate records. This contradicts the documented behavior ("Ignores entities with existing names" / "Skips duplicate relations") and breaks the implicit name-uniqueness invariant the rest of the manager relies on (e.g. addObservations/deleteEntities key on name). De-duplicate within the input batch as well, keeping the first occurrence. Co-authored-by: JSap0914 Co-authored-by: olaservo --- src/memory/__tests__/knowledge-graph.test.ts | 32 ++++++++++++++++++++ src/memory/index.ts | 20 ++++++++---- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index d1739f4..17a85aa 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -59,6 +59,20 @@ describe('KnowledgeGraphManager', () => { const newEntities = await manager.createEntities([]); expect(newEntities).toHaveLength(0); }); + + it('should ignore duplicate entity names within a single batch', async () => { + const entities: Entity[] = [ + { name: 'Alice', entityType: 'person', observations: ['first'] }, + { name: 'Alice', entityType: 'person', observations: ['second'] }, + ]; + + const newEntities = await manager.createEntities(entities); + expect(newEntities).toHaveLength(1); + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(1); + expect(graph.entities[0].name).toBe('Alice'); + }); }); describe('createRelations', () => { @@ -135,6 +149,24 @@ describe('KnowledgeGraphManager', () => { const newRelations = await manager.createRelations([]); expect(newRelations).toHaveLength(0); }); + + it('should skip duplicate relations within a single batch', async () => { + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: [] }, + { name: 'Bob', entityType: 'person', observations: [] }, + ]); + + const relations: Relation[] = [ + { from: 'Alice', to: 'Bob', relationType: 'knows' }, + { from: 'Alice', to: 'Bob', relationType: 'knows' }, + ]; + + const newRelations = await manager.createRelations(relations); + expect(newRelations).toHaveLength(1); + + const graph = await manager.readGraph(); + expect(graph.relations).toHaveLength(1); + }); }); describe('addObservations', () => { diff --git a/src/memory/index.ts b/src/memory/index.ts index 7555e4a..e0a8ce9 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -181,7 +181,11 @@ export class KnowledgeGraphManager { async createEntities(entities: Entity[]): Promise { const graph = await this.loadGraph(); - const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name)); + const newEntities = entities.filter((e, index) => + !graph.entities.some(existingEntity => existingEntity.name === e.name) && + // Also skip duplicates appearing earlier in this same batch + !entities.slice(0, index).some(earlier => earlier.name === e.name) + ); graph.entities.push(...newEntities); await this.saveGraph(graph); return newEntities; @@ -200,11 +204,15 @@ export class KnowledgeGraphManager { } }); - const newRelations = relations.filter(r => !graph.relations.some(existingRelation => - existingRelation.from === r.from && - existingRelation.to === r.to && - existingRelation.relationType === r.relationType - )); + const isSameRelation = (a: Relation, b: Relation) => + a.from === b.from && + a.to === b.to && + a.relationType === b.relationType; + const newRelations = relations.filter((r, index) => + !graph.relations.some(existingRelation => isSameRelation(existingRelation, r)) && + // Also skip duplicates appearing earlier in this same batch + !relations.slice(0, index).some(earlier => isSameRelation(earlier, r)) + ); graph.relations.push(...newRelations); await this.saveGraph(graph); return newRelations; From d73f99efbfd40c3aa1b61e88728b3d49fb52608f Mon Sep 17 00:00:00 2001 From: Nandini-Inuguru Date: Thu, 3 Sep 2026 07:12:26 +0530 Subject: [PATCH 40/40] fix(memory): serialize graph mutations to prevent concurrent write race (#4555) createEntities, createRelations, addObservations, deleteEntities, deleteObservations, and deleteRelations each independently did load -> mutate -> save with no synchronization. Concurrent tool calls (e.g. multiple mutations dispatched from one LLM turn) could race: both read the same starting state, both write back their own copy, and whichever write landed last silently discarded the other's changes. Interleaved writes could also corrupt the file outright. Adds an in-process async mutex (KnowledgeGraphManager.withLock) that serializes all six mutation methods through a single queue. Read-only methods (readGraph, searchNodes, openNodes) are unaffected. Verified: reverting the fix and re-running the new concurrency tests reproduces the bug exactly (lost entities, lost relations, malformed JSONL lines). With the fix, all 39 tests pass. Fixes #1819 Co-authored-by: olaservo --- src/memory/__tests__/knowledge-graph.test.ts | 75 ++++++++ src/memory/index.ts | 178 +++++++++++-------- 2 files changed, 180 insertions(+), 73 deletions(-) diff --git a/src/memory/__tests__/knowledge-graph.test.ts b/src/memory/__tests__/knowledge-graph.test.ts index 17a85aa..7d05a0c 100644 --- a/src/memory/__tests__/knowledge-graph.test.ts +++ b/src/memory/__tests__/knowledge-graph.test.ts @@ -696,4 +696,79 @@ describe('KnowledgeGraphManager', () => { expect(graph.entities.map(e => e.name)).toEqual(['Alice', 'Bob']); }); }); + + describe('concurrent mutations', () => { + // Regression test for #1819: concurrent tool calls each independently + // load the graph, mutate their own copy, and write it back. Without + // serialization, whichever write lands last silently discards the + // other's changes. All mutations below are fired without awaiting each + // other first, simulating multiple tool calls landing close together. + + it('should not lose entities created concurrently', async () => { + const batch1: Entity[] = Array.from({ length: 10 }, (_, i) => ({ + name: `batch1-entity-${i}`, + entityType: 'test', + observations: [], + })); + const batch2: Entity[] = Array.from({ length: 10 }, (_, i) => ({ + name: `batch2-entity-${i}`, + entityType: 'test', + observations: [], + })); + + // Fire both concurrently instead of awaiting sequentially. + await Promise.all([ + manager.createEntities(batch1), + manager.createEntities(batch2), + ]); + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(20); + expect(graph.entities.map(e => e.name).sort()).toEqual( + [...batch1, ...batch2].map(e => e.name).sort() + ); + }); + + it('should not lose relations created concurrently with entity creation', async () => { + await manager.createEntities([ + { name: 'Alice', entityType: 'person', observations: [] }, + { name: 'Bob', entityType: 'person', observations: [] }, + { name: 'Carol', entityType: 'person', observations: [] }, + ]); + + await Promise.all([ + manager.createRelations([{ from: 'Alice', to: 'Bob', relationType: 'knows' }]), + manager.createRelations([{ from: 'Bob', to: 'Carol', relationType: 'knows' }]), + manager.addObservations([ + { entityName: 'Alice', contents: ['likes coffee'] }, + ]), + ]); + + const graph = await manager.readGraph(); + expect(graph.relations).toHaveLength(2); + expect(graph.entities.find(e => e.name === 'Alice')?.observations).toContain('likes coffee'); + }); + + it('should keep the file valid JSONL after many concurrent mutations', async () => { + const operations = Array.from({ length: 25 }, (_, i) => + manager.createEntities([ + { name: `stress-entity-${i}`, entityType: 'test', observations: [] }, + ]) + ); + + await Promise.all(operations); + + const raw = await fs.readFile(testFilePath, 'utf-8'); + const lines = raw.split('\n').filter(line => line.trim() !== ''); + + // Every line must parse as valid JSON; a corrupted interleaved write + // would produce a truncated or malformed line here. + for (const line of lines) { + expect(() => JSON.parse(line)).not.toThrow(); + } + + const graph = await manager.readGraph(); + expect(graph.entities).toHaveLength(25); + }); + }); }); diff --git a/src/memory/index.ts b/src/memory/index.ts index e0a8ce9..d9f8148 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -86,6 +86,26 @@ export interface KnowledgeGraph { export class KnowledgeGraphManager { constructor(private memoryFilePath: string) {} + // Serializes all read-modify-write graph mutations behind a single queue. + // Without this, concurrent tool calls (e.g. multiple mutations dispatched + // from one LLM turn) each independently load the graph, mutate their own + // copy, and write it back — so whichever write lands last silently + // overwrites the other's changes, and interleaved writes to the same file + // can corrupt it outright. See #1819. + private mutationQueue: Promise = Promise.resolve(); + + private async withLock(operation: () => Promise): Promise { + const result = this.mutationQueue.then(operation, operation); + // Always resolve the queue itself, even if this operation failed, so a + // single failed mutation doesn't permanently wedge every call after it. + // The failure still propagates normally to whoever awaited `result`. + this.mutationQueue = result.then( + () => undefined, + () => undefined, + ); + return result; + } + private async loadGraph(): Promise { try { const data = await fs.readFile(this.memoryFilePath, "utf-8"); @@ -180,98 +200,110 @@ export class KnowledgeGraphManager { } async createEntities(entities: Entity[]): Promise { - const graph = await this.loadGraph(); - const newEntities = entities.filter((e, index) => - !graph.entities.some(existingEntity => existingEntity.name === e.name) && - // Also skip duplicates appearing earlier in this same batch - !entities.slice(0, index).some(earlier => earlier.name === e.name) - ); - graph.entities.push(...newEntities); - await this.saveGraph(graph); - return newEntities; + return this.withLock(async () => { + const graph = await this.loadGraph(); + const newEntities = entities.filter((e, index) => + !graph.entities.some(existingEntity => existingEntity.name === e.name) && + // Also skip duplicates appearing earlier in this same batch + !entities.slice(0, index).some(earlier => earlier.name === e.name) + ); + graph.entities.push(...newEntities); + await this.saveGraph(graph); + return newEntities; + }); } async createRelations(relations: Relation[]): Promise { - const graph = await this.loadGraph(); - const entityNames = new Set(graph.entities.map(e => e.name)); + return this.withLock(async () => { + const graph = await this.loadGraph(); + const entityNames = new Set(graph.entities.map(e => e.name)); - relations.forEach(r => { - if (!entityNames.has(r.from)) { - throw new Error(`Entity with name ${r.from} not found`); - } - if (!entityNames.has(r.to)) { - throw new Error(`Entity with name ${r.to} not found`); - } + relations.forEach(r => { + if (!entityNames.has(r.from)) { + throw new Error(`Entity with name ${r.from} not found`); + } + if (!entityNames.has(r.to)) { + throw new Error(`Entity with name ${r.to} not found`); + } + }); + + const isSameRelation = (a: Relation, b: Relation) => + a.from === b.from && + a.to === b.to && + a.relationType === b.relationType; + const newRelations = relations.filter((r, index) => + !graph.relations.some(existingRelation => isSameRelation(existingRelation, r)) && + // Also skip duplicates appearing earlier in this same batch + !relations.slice(0, index).some(earlier => isSameRelation(earlier, r)) + ); + graph.relations.push(...newRelations); + await this.saveGraph(graph); + return newRelations; }); - - const isSameRelation = (a: Relation, b: Relation) => - a.from === b.from && - a.to === b.to && - a.relationType === b.relationType; - const newRelations = relations.filter((r, index) => - !graph.relations.some(existingRelation => isSameRelation(existingRelation, r)) && - // Also skip duplicates appearing earlier in this same batch - !relations.slice(0, index).some(earlier => isSameRelation(earlier, r)) - ); - graph.relations.push(...newRelations); - await this.saveGraph(graph); - return newRelations; } async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { - const graph = await this.loadGraph(); - const results = observations.map(o => { - const entity = graph.entities.find(e => e.name === o.entityName); - if (!entity) { - throw new Error(`Entity with name ${o.entityName} not found`); - } - const newObservations = o.contents.filter(content => !entity.observations.includes(content)); - entity.observations.push(...newObservations); - return { entityName: o.entityName, addedObservations: newObservations }; + return this.withLock(async () => { + const graph = await this.loadGraph(); + const results = observations.map(o => { + const entity = graph.entities.find(e => e.name === o.entityName); + if (!entity) { + throw new Error(`Entity with name ${o.entityName} not found`); + } + const newObservations = o.contents.filter(content => !entity.observations.includes(content)); + entity.observations.push(...newObservations); + return { entityName: o.entityName, addedObservations: newObservations }; + }); + await this.saveGraph(graph); + return results; }); - await this.saveGraph(graph); - return results; } async deleteEntities(entityNames: string[]): Promise<{ deleted: string[]; notFound: string[] }> { - const graph = await this.loadGraph(); - const present = new Set(graph.entities.map(e => e.name)); - const deleted = entityNames.filter(name => present.has(name)); - const notFound = entityNames.filter(name => !present.has(name)); - graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); - graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); - await this.saveGraph(graph); - return { deleted, notFound }; + return this.withLock(async () => { + const graph = await this.loadGraph(); + const present = new Set(graph.entities.map(e => e.name)); + const deleted = entityNames.filter(name => present.has(name)); + const notFound = entityNames.filter(name => !present.has(name)); + graph.entities = graph.entities.filter(e => !entityNames.includes(e.name)); + graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to)); + await this.saveGraph(graph); + return { deleted, notFound }; + }); } async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<{ deletedCount: number; missingEntities: string[] }> { - const graph = await this.loadGraph(); - let deletedCount = 0; - const missingEntities: string[] = []; - deletions.forEach(d => { - const entity = graph.entities.find(e => e.name === d.entityName); - if (entity) { - const before = entity.observations.length; - entity.observations = entity.observations.filter(o => !d.observations.includes(o)); - deletedCount += before - entity.observations.length; - } else { - missingEntities.push(d.entityName); - } + return this.withLock(async () => { + const graph = await this.loadGraph(); + let deletedCount = 0; + const missingEntities: string[] = []; + deletions.forEach(d => { + const entity = graph.entities.find(e => e.name === d.entityName); + if (entity) { + const before = entity.observations.length; + entity.observations = entity.observations.filter(o => !d.observations.includes(o)); + deletedCount += before - entity.observations.length; + } else { + missingEntities.push(d.entityName); + } + }); + await this.saveGraph(graph); + return { deletedCount, missingEntities }; }); - await this.saveGraph(graph); - return { deletedCount, missingEntities }; } async deleteRelations(relations: Relation[]): Promise<{ deletedCount: number }> { - const graph = await this.loadGraph(); - const before = graph.relations.length; - graph.relations = graph.relations.filter(r => !relations.some(delRelation => - r.from === delRelation.from && - r.to === delRelation.to && - r.relationType === delRelation.relationType - )); - await this.saveGraph(graph); - return { deletedCount: before - graph.relations.length }; + return this.withLock(async () => { + const graph = await this.loadGraph(); + const before = graph.relations.length; + graph.relations = graph.relations.filter(r => !relations.some(delRelation => + r.from === delRelation.from && + r.to === delRelation.to && + r.relationType === delRelation.relationType + )); + await this.saveGraph(graph); + return { deletedCount: before - graph.relations.length }; + }); } async readGraph(): Promise {