WIP: Migrate servers to McpServer with registerTool/registerResource/registerPrompt APIs

Progress so far:
- Sequential thinking: Converted to McpServer with Zod schemas (complete)
- Memory: Converted to use registerTool API (needs Zod conversion)
- Filesystem: Converted to use registerTool API (needs Zod conversion)
- Everything: Converted to use register* APIs (needs Zod conversion)

Key learnings:
- registerTool/registerResource/registerPrompt only exist on McpServer class
- McpServer expects Zod schemas, not JSON schemas
- Updated SDK versions to ^1.20.1 for all servers

Status: Work in progress - code does not compile yet
Next steps: Convert remaining servers' JSON schemas to Zod schemas

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2025-10-18 10:15:56 +00:00
parent 7f8baf8a8c
commit bd5fcc21b6
7 changed files with 441 additions and 546 deletions

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ToolSchema,
@@ -145,17 +145,14 @@ const ToolInputSchema = ToolSchema.shape.inputSchema;
type ToolInput = z.infer<typeof ToolInputSchema>;
// Server setup
const server = new Server(
{
name: "secure-filesystem-server",
version: "0.2.0",
const server = new McpServer({
name: "secure-filesystem-server",
version: "0.2.0",
}, {
capabilities: {
tools: {},
},
{
capabilities: {
tools: {},
},
},
);
});
// Reads a file as a stream of buffers, concatenates them, and then encodes
// the result to a Base64 string. This is a memory-efficient way to handle
@@ -176,11 +173,13 @@ async function readFileAsBase64Stream(filePath: string): Promise<string> {
}
// Tool handlers
server.registerTool({
name: "read_file",
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
inputSchema: zodToJsonSchema(ReadTextFileArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"read_file",
{
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
inputSchema: ReadTextFileArgsSchema.shape
},
async (args: any) => {
const parsed = ReadTextFileArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for read_file: ${parsed.error}`);
@@ -211,20 +210,21 @@ server.registerTool({
content: [{ type: "text", text: content }],
};
}
});
);
server.registerTool({
name: "read_text_file",
description:
"Read the complete contents of a file from the file system as text. " +
"Handles various text encodings and provides detailed error messages " +
"if the file cannot be read. Use this tool when you need to examine " +
"the contents of a single file. Use the 'head' parameter to read only " +
"the first N lines of a file, or the 'tail' parameter to read only " +
"the last N lines of a file. Operates on the file as text regardless of extension. " +
"Only works within allowed directories.",
inputSchema: zodToJsonSchema(ReadTextFileArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"read_text_file",
{
description: "Read the complete contents of a file from the file system as text. " +
"Handles various text encodings and provides detailed error messages " +
"if the file cannot be read. Use this tool when you need to examine " +
"the contents of a single file. Use the 'head' parameter to read only " +
"the first N lines of a file, or the 'tail' parameter to read only " +
"the last N lines of a file. Operates on the file as text regardless of extension. " +
"Only works within allowed directories.",
inputSchema: ReadTextFileArgsSchema.shape
},
async (args: any) => {
const parsed = ReadTextFileArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for read_text_file: ${parsed.error}`);
@@ -255,15 +255,16 @@ server.registerTool({
content: [{ type: "text", text: content }],
};
}
});
);
server.registerTool({
name: "read_media_file",
description:
"Read an image or audio file. Returns the base64 encoded data and MIME type. " +
"Only works within allowed directories.",
inputSchema: zodToJsonSchema(ReadMediaFileArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"read_media_file",
{
description: "Read an image or audio file. Returns the base64 encoded data and MIME type. " +
"Only works within allowed directories.",
inputSchema: ReadMediaFileArgsSchema.shape
},
async (args: any) => {
const parsed = ReadMediaFileArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for read_media_file: ${parsed.error}`);
@@ -294,18 +295,19 @@ server.registerTool({
content: [{ type, data, mimeType }],
};
}
});
);
server.registerTool({
name: "read_multiple_files",
description:
"Read the contents of multiple files simultaneously. This is more " +
"efficient than reading files one by one when you need to analyze " +
"or compare multiple files. Each file's content is returned with its " +
"path as a reference. Failed reads for individual files won't stop " +
"the entire operation. Only works within allowed directories.",
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"read_multiple_files",
{
description: "Read the contents of multiple files simultaneously. This is more " +
"efficient than reading files one by one when you need to analyze " +
"or compare multiple files. Each file's content is returned with its " +
"path as a reference. Failed reads for individual files won't stop " +
"the entire operation. Only works within allowed directories.",
inputSchema: ReadMultipleFilesArgsSchema.shape
},
async (args: any) => {
const parsed = ReadMultipleFilesArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`);
@@ -326,16 +328,17 @@ server.registerTool({
content: [{ type: "text", text: results.join("\n---\n") }],
};
}
});
);
server.registerTool({
name: "write_file",
description:
"Create a new file or completely overwrite an existing file with new content. " +
"Use with caution as it will overwrite existing files without warning. " +
"Handles text content with proper encoding. Only works within allowed directories.",
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"write_file",
{
description: "Create a new file or completely overwrite an existing file with new content. " +
"Use with caution as it will overwrite existing files without warning. " +
"Handles text content with proper encoding. Only works within allowed directories.",
inputSchema: WriteFileArgsSchema.shape
},
async (args: any) => {
const parsed = WriteFileArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for write_file: ${parsed.error}`);
@@ -346,16 +349,17 @@ server.registerTool({
content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }],
};
}
});
);
server.registerTool({
name: "edit_file",
description:
"Make line-based edits to a text file. Each edit replaces exact line sequences " +
"with new content. Returns a git-style diff showing the changes made. " +
"Only works within allowed directories.",
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"edit_file",
{
description: "Make line-based edits to a text file. Each edit replaces exact line sequences " +
"with new content. Returns a git-style diff showing the changes made. " +
"Only works within allowed directories.",
inputSchema: EditFileArgsSchema.shape
},
async (args: any) => {
const parsed = EditFileArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for edit_file: ${parsed.error}`);
@@ -366,17 +370,18 @@ server.registerTool({
content: [{ type: "text", text: result }],
};
}
});
);
server.registerTool({
name: "create_directory",
description:
"Create a new directory or ensure a directory exists. Can create multiple " +
"nested directories in one operation. If the directory already exists, " +
"this operation will succeed silently. Perfect for setting up directory " +
"structures for projects or ensuring required paths exist. Only works within allowed directories.",
inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"create_directory",
{
description: "Create a new directory or ensure a directory exists. Can create multiple " +
"nested directories in one operation. If the directory already exists, " +
"this operation will succeed silently. Perfect for setting up directory " +
"structures for projects or ensuring required paths exist. Only works within allowed directories.",
inputSchema: CreateDirectoryArgsSchema.shape
},
async (args: any) => {
const parsed = CreateDirectoryArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for create_directory: ${parsed.error}`);
@@ -387,17 +392,18 @@ server.registerTool({
content: [{ type: "text", text: `Successfully created directory ${parsed.data.path}` }],
};
}
});
);
server.registerTool({
name: "list_directory",
description:
"Get a detailed listing of all files and directories in a specified path. " +
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
"prefixes. This tool is essential for understanding directory structure and " +
"finding specific files within a directory. Only works within allowed directories.",
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"list_directory",
{
description: "Get a detailed listing of all files and directories in a specified path. " +
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
"prefixes. This tool is essential for understanding directory structure and " +
"finding specific files within a directory. Only works within allowed directories.",
inputSchema: ListDirectoryArgsSchema.shape
},
async (args: any) => {
const parsed = ListDirectoryArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for list_directory: ${parsed.error}`);
@@ -411,17 +417,18 @@ server.registerTool({
content: [{ type: "text", text: formatted }],
};
}
});
);
server.registerTool({
name: "list_directory_with_sizes",
description:
"Get a detailed listing of all files and directories in a specified path, including sizes. " +
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
"prefixes. This tool is useful for understanding directory structure and " +
"finding specific files within a directory. Only works within allowed directories.",
inputSchema: zodToJsonSchema(ListDirectoryWithSizesArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"list_directory_with_sizes",
{
description: "Get a detailed listing of all files and directories in a specified path, including sizes. " +
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
"prefixes. This tool is useful for understanding directory structure and " +
"finding specific files within a directory. Only works within allowed directories.",
inputSchema: ListDirectoryWithSizesArgsSchema.shape
},
async (args: any) => {
const parsed = ListDirectoryWithSizesArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for list_directory_with_sizes: ${parsed.error}`);
@@ -486,17 +493,18 @@ server.registerTool({
}],
};
}
});
);
server.registerTool({
name: "directory_tree",
description:
"Get a recursive tree view of files and directories as a JSON structure. " +
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
"Files have no children array, while directories always have a children array (which may be empty). " +
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"directory_tree",
{
description: "Get a recursive tree view of files and directories as a JSON structure. " +
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
"Files have no children array, while directories always have a children array (which may be empty). " +
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
inputSchema: DirectoryTreeArgsSchema.shape
},
async (args: any) => {
const parsed = DirectoryTreeArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
@@ -553,17 +561,18 @@ server.registerTool({
}],
};
}
});
);
server.registerTool({
name: "move_file",
description:
"Move or rename files and directories. Can move files between directories " +
"and rename them in a single operation. If the destination exists, the " +
"operation will fail. Works across different directories and can be used " +
"for simple renaming within the same directory. Both source and destination must be within allowed directories.",
inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"move_file",
{
description: "Move or rename files and directories. Can move files between directories " +
"and rename them in a single operation. If the destination exists, the " +
"operation will fail. Works across different directories and can be used " +
"for simple renaming within the same directory. Both source and destination must be within allowed directories.",
inputSchema: MoveFileArgsSchema.shape
},
async (args: any) => {
const parsed = MoveFileArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for move_file: ${parsed.error}`);
@@ -575,18 +584,19 @@ server.registerTool({
content: [{ type: "text", text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }],
};
}
});
);
server.registerTool({
name: "search_files",
description:
"Recursively search for files and directories matching a pattern. " +
"The patterns should be glob-style patterns that match paths relative to the working directory. " +
"Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " +
"Returns full paths to all matching items. Great for finding files when you don't know their exact location. " +
"Only searches within allowed directories.",
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"search_files",
{
description: "Recursively search for files and directories matching a pattern. " +
"The patterns should be glob-style patterns that match paths relative to the working directory. " +
"Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " +
"Returns full paths to all matching items. Great for finding files when you don't know their exact location. " +
"Only searches within allowed directories.",
inputSchema: SearchFilesArgsSchema.shape
},
async (args: any) => {
const parsed = SearchFilesArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for search_files: ${parsed.error}`);
@@ -597,17 +607,18 @@ server.registerTool({
content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }],
};
}
});
);
server.registerTool({
name: "get_file_info",
description:
"Retrieve detailed metadata about a file or directory. Returns comprehensive " +
"information including size, creation time, last modified time, permissions, " +
"and type. This tool is perfect for understanding file characteristics " +
"without reading the actual content. Only works within allowed directories.",
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput,
handler: async (args: any) => {
server.registerTool(
"get_file_info",
{
description: "Retrieve detailed metadata about a file or directory. Returns comprehensive " +
"information including size, creation time, last modified time, permissions, " +
"and type. This tool is perfect for understanding file characteristics " +
"without reading the actual content. Only works within allowed directories.",
inputSchema: GetFileInfoArgsSchema.shape
},
async (args: any) => {
const parsed = GetFileInfoArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`);
@@ -620,21 +631,18 @@ server.registerTool({
.join("\n") }],
};
}
});
);
server.registerTool({
name: "list_allowed_directories",
description:
"Returns the list of directories that this server is allowed to access. " +
"Subdirectories within these allowed directories are also accessible. " +
"Use this to understand which directories and their nested paths are available " +
"before trying to access files.",
inputSchema: {
type: "object",
properties: {},
required: [],
server.registerTool(
"list_allowed_directories",
{
description: "Returns the list of directories that this server is allowed to access. " +
"Subdirectories within these allowed directories are also accessible. " +
"Use this to understand which directories and their nested paths are available " +
"before trying to access files.",
inputSchema: z.object({}).shape
},
handler: async (args: any) => {
async (args: any) => {
return {
content: [{
type: "text",
@@ -642,7 +650,7 @@ server.registerTool({
}],
};
}
});
);
// Updates allowed directories based on MCP client roots
async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) {

View File

@@ -24,6 +24,7 @@
"diff": "^5.1.0",
"glob": "^10.3.10",
"minimatch": "^10.0.1",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {