Skip to content

Commit 663679c

Browse files
committed
chore: update project ownership to sylphlab and bump version to 0.5.9
1 parent 5fd62f6 commit 663679c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+9331
-357
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ jobs:
117117
id: meta
118118
uses: docker/metadata-action@v5
119119
with:
120-
images: shtse8/filesystem-mcp
120+
images: sylphlab/filesystem-mcp
121121
# Use version from the build job output (which is derived from the tag)
122122
tags: |
123123
type=semver,pattern={{version}},value=${{ needs.build.outputs.version }}

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.5.9] - 2025-06-04
9+
10+
### Changed
11+
- Updated project ownership to `sylphlab`.
12+
- Updated package name to `@sylphlab/filesystem-mcp`.
13+
- Updated `README.md`, `LICENSE`, and GitHub Actions workflow (`publish.yml`) to reflect new ownership and package name.
14+
15+
816
## [0.5.8] - 2025-04-05
917

1018
### Fixed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 shtse8
3+
Copyright (c) 2025 sylphlab
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Filesystem MCP Server (@shtse8/filesystem-mcp)
1+
# Filesystem MCP Server (@sylphlab/filesystem-mcp)
22

3-
[![npm version](https://badge.fury.io/js/%40shtse8%2Ffilesystem-mcp.svg)](https://badge.fury.io/js/%40shtse8%2Ffilesystem-mcp)
4-
[![Docker Pulls](https://img.shields.io/docker/pulls/shtse8/filesystem-mcp.svg)](https://hub.docker.com/r/shtse8/filesystem-mcp)
3+
[![npm version](https://badge.fury.io/js/%40sylphlab%2Ffilesystem-mcp.svg)](https://badge.fury.io/js/%40sylphlab%2Ffilesystem-mcp)
4+
[![Docker Pulls](https://img.shields.io/docker/pulls/sylphlab/filesystem-mcp.svg)](https://hub.docker.com/r/sylphlab/filesystem-mcp)
55

66
<!-- Add other badges like License, Build Status if applicable -->
7-
<a href="https://glama.ai/mcp/servers/@shtse8/filesystem-mcp">
8-
<img width="380" height="200" src="https://glama.ai/mcp/servers/@shtse8/filesystem-mcp/badge" />
7+
<a href="https://glama.ai/mcp/servers/@sylphlab/filesystem-mcp">
8+
<img width="380" height="200" src="https://glama.ai/mcp/servers/@sylphlab/filesystem-mcp/badge" />
99
</a>
1010

1111
**Empower your AI agents (like Cline/Claude) with secure, efficient, and
@@ -66,7 +66,7 @@ using `npx`.
6666
"filesystem-mcp": {
6767
"command": "npx",
6868
"args": [
69-
"@shtse8/filesystem-mcp"
69+
"@sylphlab/filesystem-mcp"
7070
],
7171
"name": "Filesystem (npx)"
7272
}
@@ -84,7 +84,7 @@ If you prefer using Bun, you can use `bunx` instead:
8484
"filesystem-mcp": {
8585
"command": "bunx",
8686
"args": [
87-
"@shtse8/filesystem-mcp"
87+
"@sylphlab/filesystem-mcp"
8888
],
8989
"name": "Filesystem (bunx)"
9090
}
@@ -181,7 +181,7 @@ must mount your project directory to `/app` inside the container.**
181181
"--rm",
182182
"-v",
183183
"/path/to/your/project:/app",
184-
"shtse8/filesystem-mcp:latest"
184+
"sylphlab/filesystem-mcp:latest"
185185
],
186186
"name": "Filesystem (Docker)"
187187
}
@@ -198,7 +198,7 @@ must mount your project directory to `/app` inside the container.**
198198
and shell, you _might_ be able to use variables like `$PWD` (Linux/macOS),
199199
`%CD%` (Windows Cmd), or `${workspaceFolder}` (if supported by the host)
200200
instead of the explicit path, but this is not guaranteed to work universally.)
201-
- `shtse8/filesystem-mcp:latest`: Specifies the Docker image. Docker will pull
201+
- `sylphlab/filesystem-mcp:latest`: Specifies the Docker image. Docker will pull
202202
it if needed.
203203

204204
**3. Restart your MCP Host environment.**
@@ -209,7 +209,7 @@ must mount your project directory to `/app` inside the container.**
209209

210210
### Local Build (For Development)
211211

212-
1. Clone: `git clone https://github.com/shtse8/filesystem-mcp.git`
212+
1. Clone: `git clone https://github.com/sylphlab/filesystem-mcp.git`
213213
2. Install: `cd filesystem-mcp && npm install`
214214
3. Build: `npm run build`
215215
4. Configure MCP Host:
@@ -245,10 +245,10 @@ This repository uses GitHub Actions (`.github/workflows/publish.yml`) to
245245
automatically:
246246

247247
1. Publish the package to
248-
[npm](https://www.npmjs.com/package/@shtse8/filesystem-mcp) on pushes to
248+
[npm](https://www.npmjs.com/package/@sylphlab/filesystem-mcp) on pushes to
249249
`main`.
250250
2. Build and push a Docker image to
251-
[Docker Hub](https://hub.docker.com/r/shtse8/filesystem-mcp) on pushes to
251+
[Docker Hub](https://hub.docker.com/r/sylphlab/filesystem-mcp) on pushes to
252252
`main`.
253253

254254
Requires `NPM_TOKEN`, `DOCKERHUB_USERNAME`, and `DOCKERHUB_TOKEN` secrets

__tests__/handlers/copyItems.test.ts

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
22
import * as fsPromises from 'fs/promises';
33
import * as path from 'path';
4+
import * as fs from 'fs'; // Import fs for PathLike type
45
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
56
import { createTemporaryFilesystem, cleanupTemporaryFilesystem } from '../testUtils.js';
67

@@ -12,6 +13,33 @@ vi.mock('../../src/utils/pathUtils.js', () => ({
1213
resolvePath: mockResolvePath,
1314
}));
1415

16+
// Mock 'fs' module using doMock BEFORE importing the handler
17+
const mockCp = vi.fn();
18+
const mockCopyFile = vi.fn(); // For fallback testing if needed later
19+
vi.doMock('fs', async (importOriginal) => {
20+
const actualFs = await importOriginal<typeof import('fs')>();
21+
const actualFsPromises = actualFs.promises;
22+
23+
// Set default implementations to call the actual functions
24+
mockCp.mockImplementation(actualFsPromises.cp);
25+
mockCopyFile.mockImplementation(actualFsPromises.copyFile);
26+
27+
return {
28+
...actualFs,
29+
promises: {
30+
...actualFsPromises,
31+
cp: mockCp,
32+
copyFile: mockCopyFile, // Include copyFile for potential fallback tests
33+
// Add other defaults if needed
34+
stat: vi.fn().mockImplementation(actualFsPromises.stat),
35+
access: vi.fn().mockImplementation(actualFsPromises.access),
36+
readFile: vi.fn().mockImplementation(actualFsPromises.readFile),
37+
writeFile: vi.fn().mockImplementation(actualFsPromises.writeFile),
38+
mkdir: vi.fn().mockImplementation(actualFsPromises.mkdir),
39+
},
40+
};
41+
});
42+
1543
// Import the handler AFTER the mock
1644
const { copyItemsToolDefinition } = await import('../../src/handlers/copyItems.js');
1745

@@ -236,4 +264,171 @@ describe('handleCopyItems Integration Tests', () => {
236264

237265

238266

267+
268+
it('should return error when attempting to copy the project root', async () => {
269+
// Mock resolvePath to return the mocked project root for the source
270+
mockResolvePath.mockImplementation((relativePath: string): string => {
271+
if (relativePath === 'try_root_source') {
272+
return 'mocked/project/root'; // Return the mocked root for source
273+
}
274+
// Default behavior for other paths (including destination)
275+
const absolutePath = path.resolve(tempRootDir, relativePath);
276+
if (!absolutePath.startsWith(tempRootDir)) {
277+
throw new McpError(ErrorCode.InvalidRequest, `Mocked Path traversal detected for ${relativePath}`);
278+
}
279+
return absolutePath;
280+
});
281+
282+
const request = { operations: [{ source: 'try_root_source', destination: 'some_dest' }] };
283+
const rawResult = await copyItemsToolDefinition.handler(request);
284+
const result = JSON.parse(rawResult.content[0].text);
285+
286+
expect(result).toHaveLength(1);
287+
expect(result[0].success).toBe(false);
288+
expect(result[0].error).toMatch(/Copying the project root is not allowed/);
289+
});
290+
291+
describe.skip('fs.cp Fallback Tests (Node < 16.7)', () => {
292+
let originalCp: any;
293+
294+
beforeEach(() => {
295+
// Store original and remove fs.cp
296+
originalCp = fsPromises.cp;
297+
(fsPromises as any).cp = undefined;
298+
});
299+
300+
afterEach(() => {
301+
// Restore original fs.cp
302+
(fsPromises as any).cp = originalCp;
303+
vi.restoreAllMocks(); // Restore any spies used within tests
304+
});
305+
306+
it('should fail to copy a directory using fallback', async () => {
307+
const request = { operations: [{ source: 'dirToCopy', destination: 'fallbackDirFail' }] };
308+
const rawResult = await copyItemsToolDefinition.handler(request);
309+
const result = JSON.parse(rawResult.content[0].text);
310+
311+
expect(result).toHaveLength(1);
312+
expect(result[0].success).toBe(false);
313+
expect(result[0].error).toMatch(/Recursive directory copy requires Node.js 16.7+/);
314+
// Verify destination was not created
315+
await expect(fsPromises.access(path.join(tempRootDir, 'fallbackDirFail'))).rejects.toThrow();
316+
});
317+
318+
it('should copy a file using fallback fs.copyFile', async () => {
319+
// Spy on copyFile to ensure it's called
320+
const copyFileSpy = vi.spyOn(fsPromises, 'copyFile');
321+
322+
const request = { operations: [{ source: 'fileToCopy.txt', destination: 'fallbackFileSuccess.txt' }] };
323+
const rawResult = await copyItemsToolDefinition.handler(request);
324+
const result = JSON.parse(rawResult.content[0].text);
325+
326+
expect(result).toHaveLength(1);
327+
expect(result[0].success).toBe(true);
328+
expect(copyFileSpy).toHaveBeenCalledOnce(); // Verify fs.copyFile was used
329+
330+
// Verify copy
331+
const content = await fsPromises.readFile(path.join(tempRootDir, 'fallbackFileSuccess.txt'), 'utf-8');
332+
expect(content).toBe('Copy me!');
333+
});
334+
});
335+
336+
it('should handle permission errors during copy', async () => {
337+
const sourceFile = 'fileToCopy.txt';
338+
const destFile = 'perm_denied_dest.txt';
339+
const sourcePath = path.join(tempRootDir, sourceFile);
340+
const destPath = path.join(tempRootDir, destFile);
341+
342+
// Configure the mockCp for this specific test
343+
mockCp.mockImplementation(async (src: string | URL, dest: string | URL, opts?: fs.CopyOptions) => { // Use string | URL
344+
if (src.toString() === sourcePath && dest.toString() === destPath) {
345+
const error: NodeJS.ErrnoException = new Error('Mocked EPERM during copy');
346+
error.code = 'EPERM';
347+
throw error;
348+
}
349+
// Fallback to default (actual cp) if needed, though unlikely in this specific test
350+
const actualFsPromises = (await vi.importActual<typeof import('fs')>('fs')).promises;
351+
return actualFsPromises.cp(src, dest, opts);
352+
});
353+
354+
const request = { operations: [{ source: sourceFile, destination: destFile }] };
355+
const rawResult = await copyItemsToolDefinition.handler(request);
356+
const result = JSON.parse(rawResult.content[0].text);
357+
358+
expect(result).toHaveLength(1);
359+
expect(result[0].success).toBe(false);
360+
// Adjust assertion to match the actual error message format from the handler
361+
expect(result[0].error).toMatch(/Permission denied copying 'fileToCopy.txt' to 'perm_denied_dest.txt'/);
362+
// Check that our mock function was called with the resolved paths
363+
expect(mockCp).toHaveBeenCalledWith(sourcePath, destPath, { recursive: true, errorOnExist: false, force: true }); // Match handler options
364+
365+
// vi.clearAllMocks() in afterEach handles cleanup
366+
});
367+
368+
it('should handle generic errors during copy', async () => {
369+
const sourceFile = 'fileToCopy.txt';
370+
const destFile = 'generic_error_dest.txt';
371+
const sourcePath = path.join(tempRootDir, sourceFile);
372+
const destPath = path.join(tempRootDir, destFile);
373+
374+
// Configure the mockCp for this specific test
375+
mockCp.mockImplementation(async (src: string | URL, dest: string | URL, opts?: fs.CopyOptions) => { // Use string | URL
376+
if (src.toString() === sourcePath && dest.toString() === destPath) {
377+
throw new Error('Mocked generic copy error');
378+
}
379+
// Fallback to default (actual cp) if needed
380+
const actualFsPromises = (await vi.importActual<typeof import('fs')>('fs')).promises;
381+
return actualFsPromises.cp(src, dest, opts);
382+
});
383+
384+
const request = { operations: [{ source: sourceFile, destination: destFile }] };
385+
const rawResult = await copyItemsToolDefinition.handler(request);
386+
const result = JSON.parse(rawResult.content[0].text);
387+
388+
expect(result).toHaveLength(1);
389+
expect(result[0].success).toBe(false);
390+
expect(result[0].error).toMatch(/Failed to copy item: Mocked generic copy error/);
391+
// Check that our mock function was called with the resolved paths
392+
expect(mockCp).toHaveBeenCalledWith(sourcePath, destPath, { recursive: true, errorOnExist: false, force: true }); // Match handler options
393+
394+
// vi.clearAllMocks() in afterEach handles cleanup
395+
});
396+
397+
it('should handle unexpected errors during path resolution within the map', async () => {
398+
// Mock resolvePath to throw a generic error for a specific path *after* initial validation
399+
mockResolvePath.mockImplementation((relativePath: string): string => {
400+
if (relativePath === 'unexpected_resolve_error_dest') {
401+
throw new Error('Mocked unexpected resolve error');
402+
}
403+
// Default behavior
404+
const absolutePath = path.resolve(tempRootDir, relativePath);
405+
if (!absolutePath.startsWith(tempRootDir)) {
406+
throw new McpError(ErrorCode.InvalidRequest, `Mocked Path traversal detected for ${relativePath}`);
407+
}
408+
return absolutePath;
409+
});
410+
411+
const request = { operations: [
412+
{ source: 'fileToCopy.txt', destination: 'goodDest.txt'},
413+
{ source: 'anotherFile.txt', destination: 'unexpected_resolve_error_dest' }
414+
]};
415+
const rawResult = await copyItemsToolDefinition.handler(request);
416+
const result = JSON.parse(rawResult.content[0].text);
417+
418+
expect(result).toHaveLength(2);
419+
420+
const goodResult = result.find((r: any) => r.destination === 'goodDest.txt');
421+
expect(goodResult).toBeDefined();
422+
expect(goodResult.success).toBe(true);
423+
424+
const errorResult = result.find((r: any) => r.destination === 'unexpected_resolve_error_dest');
425+
expect(errorResult).toBeDefined();
426+
expect(errorResult.success).toBe(false);
427+
// This error is caught by the inner try/catch (lines 93-94)
428+
expect(errorResult.error).toMatch(/Failed to copy item: Mocked unexpected resolve error/);
429+
430+
// Verify the successful copy occurred
431+
await expect(fsPromises.access(path.join(tempRootDir, 'goodDest.txt'))).resolves.toBeUndefined();
432+
});
433+
239434
});

0 commit comments

Comments
 (0)