From f862bbdcc7c4c1c1ed978c2ae26dab608eeca418 Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Sun, 3 May 2026 15:44:52 +0200 Subject: [PATCH] Add Resources and Prompts to filesystem server --- src/filesystem/README.md | 24 ++++++++++++++++++++++ src/filesystem/__tests__/prompts.test.ts | 11 ++++++++++ src/filesystem/__tests__/resources.test.ts | 20 ++++++++++++++++++ src/filesystem/index.ts | 5 +++++ src/filesystem/prompts.ts | 8 ++++++++ src/filesystem/resources.ts | 14 +++++++++++++ 6 files changed, 82 insertions(+) create mode 100644 src/filesystem/__tests__/prompts.test.ts create mode 100644 src/filesystem/__tests__/resources.test.ts create mode 100644 src/filesystem/prompts.ts create mode 100644 src/filesystem/resources.ts diff --git a/src/filesystem/README.md b/src/filesystem/README.md index c099da1e8c..dce3ff22bc 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -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 @@ -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`: diff --git a/src/filesystem/__tests__/prompts.test.ts b/src/filesystem/__tests__/prompts.test.ts new file mode 100644 index 0000000000..320c62f2a5 --- /dev/null +++ b/src/filesystem/__tests__/prompts.test.ts @@ -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'); +}); diff --git a/src/filesystem/__tests__/resources.test.ts b/src/filesystem/__tests__/resources.test.ts new file mode 100644 index 0000000000..c863824843 --- /dev/null +++ b/src/filesystem/__tests__/resources.test.ts @@ -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); +}); diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 7b67e63e58..2646a1aa03 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -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); @@ -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); diff --git a/src/filesystem/prompts.ts b/src/filesystem/prompts.ts new file mode 100644 index 0000000000..47435d8fdc --- /dev/null +++ b/src/filesystem/prompts.ts @@ -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.` } }] })); +}; diff --git a/src/filesystem/resources.ts b/src/filesystem/resources.ts new file mode 100644 index 0000000000..fc845a02f4 --- /dev/null +++ b/src/filesystem/resources.ts @@ -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) }] }; + }); +};