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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/filesystem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio
- Search files
- Get file metadata
- Dynamic directory access control via [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots)
- Expose allowed directories and files as MCP Resources
- Built-in prompt for file review

## Directory Access Control

Expand Down Expand Up @@ -204,6 +206,28 @@ The mapping for filesystem tools is:

> Note: `idempotentHint` and `destructiveHint` are meaningful only when `readOnlyHint` is `false`, as defined by the MCP spec.

### Resources

- **filesystem://allowed-directories**
- Returns the list of directories the server is currently allowed to access
- No arguments required
- Returns a plain text list with one directory per line
- Reflects the live state of allowed directories, whether set via command-line arguments or updated at runtime via MCP Roots

- **file://{path}**
- Reads a file within the allowed directories and returns its contents as a resource
- URI format: `file:///absolute/path/to/file`
- Returns the file contents as plain text
- The path is validated against allowed directories before the file is read

### Prompts

- **read-file**
- Asks the model to read a file and summarize its contents
- Arguments:
- `path` (string): Absolute path to the file
- Returns a user message that instructs the model to read and summarize the file at the given path

## Usage with Claude Desktop
Add this to your `claude_desktop_config.json`:

Expand Down
11 changes: 11 additions & 0 deletions src/filesystem/__tests__/prompts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { it, expect, vi } from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerPrompts } from '../prompts.js';

it('registers read-file prompt with path in message', () => {
const s = { registerPrompt: vi.fn() } as unknown as McpServer;
registerPrompts(s);
const [name,, handler] = (s.registerPrompt as any).mock.calls[0];
expect(name).toBe('read-file');
expect(handler({ path: '/a/b.txt' }).messages[0].content.text).toContain('/a/b.txt');
});
20 changes: 20 additions & 0 deletions src/filesystem/__tests__/resources.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { it, expect, beforeEach, afterEach, vi } from 'vitest';
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { mkdtempSync, rmSync, realpathSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { setAllowedDirectories } from '../lib.js';
import { registerResources } from '../resources.js';

let dir: string;
beforeEach(() => { dir = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-fs-'))); setAllowedDirectories([dir]); });
afterEach(() => { rmSync(dir, { recursive: true, force: true }); setAllowedDirectories([]); });

it('registers allowed-directories and file resources', () => {
const s = { registerResource: vi.fn() } as unknown as McpServer;
registerResources(s);
const calls = (s.registerResource as any).mock.calls;
expect(calls).toHaveLength(2);
expect(calls.map((c: any) => c[0])).toEqual(expect.arrayContaining(['allowed-directories', 'file']));
expect(calls.find((c: any) => c[0] === 'file')[1]).toBeInstanceOf(ResourceTemplate);
});
5 changes: 5 additions & 0 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
headFile,
setAllowedDirectories,
} from './lib.js';
import { registerResources } from './resources.js';
import { registerPrompts } from './prompts.js';

// Command line argument parsing
const args = process.argv.slice(2);
Expand Down Expand Up @@ -702,6 +704,9 @@ server.registerTool(
}
);

registerResources(server);
registerPrompts(server);

// Updates allowed directories based on MCP client roots
async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) {
const validatedRootDirs = await getValidRootDirectories(requestedRoots);
Expand Down
8 changes: 8 additions & 0 deletions src/filesystem/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export const registerPrompts = (server: McpServer): void => {
server.registerPrompt("read-file",
{ title: "Read File", description: "Read a file for review.", argsSchema: { path: z.string().describe("Absolute file path") } },
({ path }) => ({ messages: [{ role: "user", content: { type: "text", text: `Read ${path} and summarize.` } }] }));
};
14 changes: 14 additions & 0 deletions src/filesystem/resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { validatePath, readFileContent, getAllowedDirectories } from "./lib.js";

export const registerResources = (server: McpServer): void => {
server.registerResource("allowed-directories", "filesystem://allowed-directories",
{ title: "Allowed Directories", description: "Allowed access directories.", mimeType: "text/plain" },
async (uri) => ({ contents: [{ uri: uri.toString(), mimeType: "text/plain", text: getAllowedDirectories().join("\n") || "None configured" }] }));
server.registerResource("file", new ResourceTemplate("file://{path}", { list: undefined }),
{ title: "File", description: "Read a file. URI: file:///path/to/file", mimeType: "text/plain" },
async (uri, v) => {
const validPath = await validatePath(String(v.path ?? ""));
return { contents: [{ uri: uri.toString(), mimeType: "text/plain", text: await readFileContent(validPath) }] };
});
};
Loading