diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 2300ee04..0b3d69b6 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -1,25 +1,17 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { - CallToolRequestSchema, ClientCapabilities, CompleteRequestSchema, CreateMessageRequest, CreateMessageResultSchema, ElicitResultSchema, - GetPromptRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListResourceTemplatesRequestSchema, - ListToolsRequestSchema, LoggingLevel, - ReadResourceRequestSchema, Resource, RootsListChangedNotificationSchema, ServerNotification, ServerRequest, SubscribeRequestSchema, - Tool, ToolSchema, UnsubscribeRequestSchema, type Root @@ -280,57 +272,17 @@ export const createServer = () => { const PAGE_SIZE = 10; - server.setRequestHandler(ListResourcesRequestSchema, async (request) => { - const cursor = request.params?.cursor; - let startIndex = 0; - - if (cursor) { - const decodedCursor = parseInt(atob(cursor), 10); - if (!isNaN(decodedCursor)) { - startIndex = decodedCursor; + // Register all resources dynamically + ALL_RESOURCES.forEach(resource => { + server.registerResource({ + uri: resource.uri, + name: resource.name, + mimeType: resource.mimeType, + description: `Static resource ${resource.name}`, + handler: async () => { + return resource.text ? { text: resource.text } : { blob: resource.blob }; } - } - - const endIndex = Math.min(startIndex + PAGE_SIZE, ALL_RESOURCES.length); - const resources = ALL_RESOURCES.slice(startIndex, endIndex); - - let nextCursor: string | undefined; - if (endIndex < ALL_RESOURCES.length) { - nextCursor = btoa(endIndex.toString()); - } - - return { - resources, - nextCursor, - }; - }); - - server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { - return { - resourceTemplates: [ - { - uriTemplate: "test://static/resource/{id}", - name: "Static Resource", - description: "A static resource with a numeric ID", - }, - ], - }; - }); - - server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - const uri = request.params.uri; - - if (uri.startsWith("test://static/resource/")) { - const index = parseInt(uri.split("/").pop() ?? "", 10) - 1; - if (index >= 0 && index < ALL_RESOURCES.length) { - const resource = ALL_RESOURCES[index]; - return { - contents: [resource], - }; - } - } - - throw new Error(`Unknown resource: ${uri}`); + }); }); server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => { @@ -344,48 +296,11 @@ export const createServer = () => { return {}; }); - server.setRequestHandler(ListPromptsRequestSchema, async () => { - return { - prompts: [ - { - name: PromptName.SIMPLE, - description: "A prompt without arguments", - }, - { - name: PromptName.COMPLEX, - description: "A prompt with arguments", - arguments: [ - { - name: "temperature", - description: "Temperature setting", - required: true, - }, - { - name: "style", - description: "Output style", - required: false, - }, - ], - }, - { - name: PromptName.RESOURCE, - description: "A prompt that includes an embedded resource reference", - arguments: [ - { - name: "resourceId", - description: "Resource ID to include (1-100)", - required: true, - }, - ], - }, - ], - }; - }); - - server.setRequestHandler(GetPromptRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - if (name === PromptName.SIMPLE) { + // Register prompts + server.registerPrompt({ + name: PromptName.SIMPLE, + description: "A prompt without arguments", + handler: async () => { return { messages: [ { @@ -398,8 +313,24 @@ export const createServer = () => { ], }; } + }); - if (name === PromptName.COMPLEX) { + server.registerPrompt({ + name: PromptName.COMPLEX, + description: "A prompt with arguments", + arguments: [ + { + name: "temperature", + description: "Temperature setting", + required: true, + }, + { + name: "style", + description: "Output style", + required: false, + }, + ], + handler: async (args) => { return { messages: [ { @@ -427,8 +358,19 @@ export const createServer = () => { ], }; } + }); - if (name === PromptName.RESOURCE) { + server.registerPrompt({ + name: PromptName.RESOURCE, + description: "A prompt that includes an embedded resource reference", + arguments: [ + { + name: "resourceId", + description: "Resource ID to include (1-100)", + required: true, + }, + ], + handler: async (args) => { const resourceId = parseInt(args?.resourceId as string, 10); if (isNaN(resourceId) || resourceId < 1 || resourceId > 100) { throw new Error( @@ -458,101 +400,26 @@ export const createServer = () => { ], }; } - - throw new Error(`Unknown prompt: ${name}`); }); - server.setRequestHandler(ListToolsRequestSchema, async () => { - const tools: Tool[] = [ - { - name: ToolName.ECHO, - description: "Echoes back the input", - inputSchema: zodToJsonSchema(EchoSchema) as ToolInput, - }, - { - name: ToolName.ADD, - description: "Adds two numbers", - inputSchema: zodToJsonSchema(AddSchema) as ToolInput, - }, - { - name: ToolName.LONG_RUNNING_OPERATION, - description: - "Demonstrates a long running operation with progress updates", - inputSchema: zodToJsonSchema(LongRunningOperationSchema) as ToolInput, - }, - { - name: ToolName.PRINT_ENV, - description: - "Prints all environment variables, helpful for debugging MCP server configuration", - inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput, - }, - { - name: ToolName.SAMPLE_LLM, - description: "Samples from an LLM using MCP's sampling feature", - inputSchema: zodToJsonSchema(SampleLLMSchema) as ToolInput, - }, - { - name: ToolName.GET_TINY_IMAGE, - description: "Returns the MCP_TINY_IMAGE", - inputSchema: zodToJsonSchema(GetTinyImageSchema) as ToolInput, - }, - { - name: ToolName.ANNOTATED_MESSAGE, - description: - "Demonstrates how annotations can be used to provide metadata about content", - inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput, - }, - { - name: ToolName.GET_RESOURCE_REFERENCE, - description: - "Returns a resource reference that can be used by MCP clients", - inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput, - }, - { - name: ToolName.GET_RESOURCE_LINKS, - description: - "Returns multiple resource links that reference different types of resources", - inputSchema: zodToJsonSchema(GetResourceLinksSchema) as ToolInput, - }, - { - name: ToolName.STRUCTURED_CONTENT, - description: - "Returns structured content along with an output schema for client data validation", - inputSchema: zodToJsonSchema(StructuredContentSchema.input) as ToolInput, - outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput, - }, - { - name: ToolName.ZIP_RESOURCES, - description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.", - inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput, - } - ]; - if (clientCapabilities!.roots) tools.push ({ - name: ToolName.LIST_ROOTS, - description: - "Lists the current MCP roots provided by the client. Demonstrates the roots protocol capability even though this server doesn't access files.", - inputSchema: zodToJsonSchema(ListRootsSchema) as ToolInput, - }); - if (clientCapabilities!.elicitation) tools.push ({ - name: ToolName.ELICITATION, - description: "Elicitation test tool that demonstrates how to request user input with various field types (string, boolean, email, uri, date, integer, number, enum)", - inputSchema: zodToJsonSchema(ElicitationSchema) as ToolInput, - }); - - return { tools }; - }); - - server.setRequestHandler(CallToolRequestSchema, async (request,extra) => { - const { name, arguments: args } = request.params; - - if (name === ToolName.ECHO) { + // Register tools + server.registerTool({ + name: ToolName.ECHO, + description: "Echoes back the input", + inputSchema: zodToJsonSchema(EchoSchema) as ToolInput, + handler: async (args) => { const validatedArgs = EchoSchema.parse(args); return { content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }], }; } + }); - if (name === ToolName.ADD) { + server.registerTool({ + name: ToolName.ADD, + description: "Adds two numbers", + inputSchema: zodToJsonSchema(AddSchema) as ToolInput, + handler: async (args) => { const validatedArgs = AddSchema.parse(args); const sum = validatedArgs.a + validatedArgs.b; return { @@ -564,12 +431,17 @@ export const createServer = () => { ], }; } + }); - if (name === ToolName.LONG_RUNNING_OPERATION) { + server.registerTool({ + name: ToolName.LONG_RUNNING_OPERATION, + description: "Demonstrates a long running operation with progress updates", + inputSchema: zodToJsonSchema(LongRunningOperationSchema) as ToolInput, + handler: async (args, extra) => { const validatedArgs = LongRunningOperationSchema.parse(args); const { duration, steps } = validatedArgs; const stepDuration = duration / steps; - const progressToken = request.params._meta?.progressToken; + const progressToken = extra.request.params._meta?.progressToken; for (let i = 1; i < steps + 1; i++) { await new Promise((resolve) => @@ -597,8 +469,13 @@ export const createServer = () => { ], }; } + }); - if (name === ToolName.PRINT_ENV) { + server.registerTool({ + name: ToolName.PRINT_ENV, + description: "Prints all environment variables, helpful for debugging MCP server configuration", + inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput, + handler: async () => { return { content: [ { @@ -608,8 +485,13 @@ export const createServer = () => { ], }; } + }); - if (name === ToolName.SAMPLE_LLM) { + server.registerTool({ + name: ToolName.SAMPLE_LLM, + description: "Samples from an LLM using MCP's sampling feature", + inputSchema: zodToJsonSchema(SampleLLMSchema) as ToolInput, + handler: async (args, extra) => { const validatedArgs = SampleLLMSchema.parse(args); const { prompt, maxTokens } = validatedArgs; @@ -625,8 +507,13 @@ export const createServer = () => { ], }; } + }); - if (name === ToolName.GET_TINY_IMAGE) { + server.registerTool({ + name: ToolName.GET_TINY_IMAGE, + description: "Returns the MCP_TINY_IMAGE", + inputSchema: zodToJsonSchema(GetTinyImageSchema) as ToolInput, + handler: async (args) => { GetTinyImageSchema.parse(args); return { content: [ @@ -646,8 +533,13 @@ export const createServer = () => { ], }; } + }); - if (name === ToolName.ANNOTATED_MESSAGE) { + server.registerTool({ + name: ToolName.ANNOTATED_MESSAGE, + description: "Demonstrates how annotations can be used to provide metadata about content", + inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput, + handler: async (args) => { const { messageType, includeImage } = AnnotatedMessageSchema.parse(args); const content = []; @@ -697,8 +589,13 @@ export const createServer = () => { return { content }; } + }); - if (name === ToolName.GET_RESOURCE_REFERENCE) { + server.registerTool({ + name: ToolName.GET_RESOURCE_REFERENCE, + description: "Returns a resource reference that can be used by MCP clients", + inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput, + handler: async (args) => { const validatedArgs = GetResourceReferenceSchema.parse(args); const resourceId = validatedArgs.resourceId; @@ -726,128 +623,13 @@ export const createServer = () => { ], }; } + }); - if (name === ToolName.ELICITATION) { - ElicitationSchema.parse(args); - - const elicitationResult = await extra.sendRequest({ - method: 'elicitation/create', - params: { - message: 'Please provide inputs for the following fields:', - requestedSchema: { - type: 'object', - properties: { - name: { - title: 'Full Name', - type: 'string', - description: 'Your full, legal name', - }, - check: { - title: 'Agree to terms', - type: 'boolean', - description: 'A boolean check', - }, - color: { - title: 'Favorite Color', - type: 'string', - description: 'Favorite color (open text)', - default: 'blue', - }, - email: { - title: 'Email Address', - type: 'string', - format: 'email', - description: 'Your email address (will be verified, and never shared with anyone else)', - }, - homepage: { - type: 'string', - format: 'uri', - description: 'Homepage / personal site', - }, - birthdate: { - title: 'Birthdate', - type: 'string', - format: 'date', - description: 'Your date of birth (will never be shared with anyone else)', - }, - integer: { - title: 'Favorite Integer', - type: 'integer', - description: 'Your favorite integer (do not give us your phone number, pin, or other sensitive info)', - minimum: 1, - maximum: 100, - default: 42, - }, - number: { - title: 'Favorite Number', - type: 'number', - description: 'Favorite number (there are no wrong answers)', - minimum: 0, - maximum: 1000, - default: 3.14, - }, - petType: { - title: 'Pet type', - type: 'string', - enum: ['cats', 'dogs', 'birds', 'fish', 'reptiles'], - enumNames: ['Cats', 'Dogs', 'Birds', 'Fish', 'Reptiles'], - default: 'dogs', - description: 'Your favorite pet type', - }, - }, - required: ['name'], - }, - }, - }, ElicitResultSchema, { timeout: 10 * 60 * 1000 /* 10 minutes */ }); - - // Handle different response actions - const content = []; - - if (elicitationResult.action === 'accept' && elicitationResult.content) { - content.push({ - type: "text", - text: `✅ User provided the requested information!`, - }); - - // Only access elicitationResult.content when action is accept - const userData = elicitationResult.content; - const lines = []; - if (userData.name) lines.push(`- Name: ${userData.name}`); - if (userData.check !== undefined) lines.push(`- Agreed to terms: ${userData.check}`); - if (userData.color) lines.push(`- Favorite Color: ${userData.color}`); - if (userData.email) lines.push(`- Email: ${userData.email}`); - if (userData.homepage) lines.push(`- Homepage: ${userData.homepage}`); - if (userData.birthdate) lines.push(`- Birthdate: ${userData.birthdate}`); - if (userData.integer !== undefined) lines.push(`- Favorite Integer: ${userData.integer}`); - if (userData.number !== undefined) lines.push(`- Favorite Number: ${userData.number}`); - if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`); - - content.push({ - type: "text", - text: `User inputs:\n${lines.join('\n')}`, - }); - } else if (elicitationResult.action === 'decline') { - content.push({ - type: "text", - text: `❌ User declined to provide the requested information.`, - }); - } else if (elicitationResult.action === 'cancel') { - content.push({ - type: "text", - text: `⚠️ User cancelled the elicitation dialog.`, - }); - } - - // Include raw result for debugging - content.push({ - type: "text", - text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`, - }); - - return { content }; - } - - if (name === ToolName.GET_RESOURCE_LINKS) { + server.registerTool({ + name: ToolName.GET_RESOURCE_LINKS, + description: "Returns multiple resource links that reference different types of resources", + inputSchema: zodToJsonSchema(GetResourceLinksSchema) as ToolInput, + handler: async (args) => { const { count } = GetResourceLinksSchema.parse(args); const content = []; @@ -875,8 +657,14 @@ export const createServer = () => { return { content }; } + }); - if (name === ToolName.STRUCTURED_CONTENT) { + server.registerTool({ + name: ToolName.STRUCTURED_CONTENT, + description: "Returns structured content along with an output schema for client data validation", + inputSchema: zodToJsonSchema(StructuredContentSchema.input) as ToolInput, + outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput, + handler: async (args) => { // The same response is returned for every input. const validatedArgs = StructuredContentSchema.input.parse(args); @@ -896,8 +684,13 @@ export const createServer = () => { structuredContent: weather }; } + }); - if (name === ToolName.ZIP_RESOURCES) { + server.registerTool({ + name: ToolName.ZIP_RESOURCES, + description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.", + inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput, + handler: async (args) => { const { files } = ZipResourcesInputSchema.parse(args); const zip = new JSZip(); @@ -927,54 +720,6 @@ export const createServer = () => { ], }; } - - if (name === ToolName.LIST_ROOTS) { - ListRootsSchema.parse(args); - - if (!clientSupportsRoots) { - return { - content: [ - { - type: "text", - text: "The MCP client does not support the roots protocol.\n\n" + - "This means the server cannot access information about the client's workspace directories or file system roots." - } - ] - }; - } - - if (currentRoots.length === 0) { - return { - content: [ - { - type: "text", - text: "The client supports roots but no roots are currently configured.\n\n" + - "This could mean:\n" + - "1. The client hasn't provided any roots yet\n" + - "2. The client provided an empty roots list\n" + - "3. The roots configuration is still being loaded" - } - ] - }; - } - - const rootsList = currentRoots.map((root, index) => { - return `${index + 1}. ${root.name || 'Unnamed Root'}\n URI: ${root.uri}`; - }).join('\n\n'); - - return { - content: [ - { - type: "text", - text: `Current MCP Roots (${currentRoots.length} total):\n\n${rootsList}\n\n` + - "Note: This server demonstrates the roots protocol capability but doesn't actually access files. " + - "The roots are provided by the MCP client and can be used by servers that need file system access." - } - ] - }; - } - - throw new Error(`Unknown tool: ${name}`); }); server.setRequestHandler(CompleteRequestSchema, async (request) => { @@ -1030,12 +775,66 @@ export const createServer = () => { } }); - // Handle post-initialization setup for roots + // Handle post-initialization setup for roots and conditional tools server.oninitialized = async () => { clientCapabilities = server.getClientCapabilities(); + // Register conditional tools based on client capabilities if (clientCapabilities?.roots) { clientSupportsRoots = true; + + // Register LIST_ROOTS tool + server.registerTool({ + name: ToolName.LIST_ROOTS, + description: "Lists the current MCP roots provided by the client. Demonstrates the roots protocol capability even though this server doesn't access files.", + inputSchema: zodToJsonSchema(ListRootsSchema) as ToolInput, + handler: async (args) => { + ListRootsSchema.parse(args); + + if (!clientSupportsRoots) { + return { + content: [ + { + type: "text", + text: "The MCP client does not support the roots protocol.\n\n" + + "This means the server cannot access information about the client's workspace directories or file system roots." + } + ] + }; + } + + if (currentRoots.length === 0) { + return { + content: [ + { + type: "text", + text: "The client supports roots but no roots are currently configured.\n\n" + + "This could mean:\n" + + "1. The client hasn't provided any roots yet\n" + + "2. The client provided an empty roots list\n" + + "3. The roots configuration is still being loaded" + } + ] + }; + } + + const rootsList = currentRoots.map((root, index) => { + return `${index + 1}. ${root.name || 'Unnamed Root'}\n URI: ${root.uri}`; + }).join('\n\n'); + + return { + content: [ + { + type: "text", + text: `Current MCP Roots (${currentRoots.length} total):\n\n${rootsList}\n\n` + + "Note: This server demonstrates the roots protocol capability but doesn't actually access files. " + + "The roots are provided by the MCP client and can be used by servers that need file system access." + } + ] + }; + } + }); + try { const response = await server.listRoots(); if (response && 'roots' in response) { @@ -1067,6 +866,134 @@ export const createServer = () => { data: "Client does not support MCP roots protocol", }, sessionId); } + + if (clientCapabilities?.elicitation) { + // Register ELICITATION tool + server.registerTool({ + name: ToolName.ELICITATION, + description: "Elicitation test tool that demonstrates how to request user input with various field types (string, boolean, email, uri, date, integer, number, enum)", + inputSchema: zodToJsonSchema(ElicitationSchema) as ToolInput, + handler: async (args, extra) => { + ElicitationSchema.parse(args); + + const elicitationResult = await extra.sendRequest({ + method: 'elicitation/create', + params: { + message: 'Please provide inputs for the following fields:', + requestedSchema: { + type: 'object', + properties: { + name: { + title: 'Full Name', + type: 'string', + description: 'Your full, legal name', + }, + check: { + title: 'Agree to terms', + type: 'boolean', + description: 'A boolean check', + }, + color: { + title: 'Favorite Color', + type: 'string', + description: 'Favorite color (open text)', + default: 'blue', + }, + email: { + title: 'Email Address', + type: 'string', + format: 'email', + description: 'Your email address (will be verified, and never shared with anyone else)', + }, + homepage: { + type: 'string', + format: 'uri', + description: 'Homepage / personal site', + }, + birthdate: { + title: 'Birthdate', + type: 'string', + format: 'date', + description: 'Your date of birth (will never be shared with anyone else)', + }, + integer: { + title: 'Favorite Integer', + type: 'integer', + description: 'Your favorite integer (do not give us your phone number, pin, or other sensitive info)', + minimum: 1, + maximum: 100, + default: 42, + }, + number: { + title: 'Favorite Number', + type: 'number', + description: 'Favorite number (there are no wrong answers)', + minimum: 0, + maximum: 1000, + default: 3.14, + }, + petType: { + title: 'Pet type', + type: 'string', + enum: ['cats', 'dogs', 'birds', 'fish', 'reptiles'], + enumNames: ['Cats', 'Dogs', 'Birds', 'Fish', 'Reptiles'], + default: 'dogs', + description: 'Your favorite pet type', + }, + }, + required: ['name'], + }, + }, + }, ElicitResultSchema, { timeout: 10 * 60 * 1000 /* 10 minutes */ }); + + // Handle different response actions + const content = []; + + if (elicitationResult.action === 'accept' && elicitationResult.content) { + content.push({ + type: "text", + text: `✅ User provided the requested information!`, + }); + + // Only access elicitationResult.content when action is accept + const userData = elicitationResult.content; + const lines = []; + if (userData.name) lines.push(`- Name: ${userData.name}`); + if (userData.check !== undefined) lines.push(`- Agreed to terms: ${userData.check}`); + if (userData.color) lines.push(`- Favorite Color: ${userData.color}`); + if (userData.email) lines.push(`- Email: ${userData.email}`); + if (userData.homepage) lines.push(`- Homepage: ${userData.homepage}`); + if (userData.birthdate) lines.push(`- Birthdate: ${userData.birthdate}`); + if (userData.integer !== undefined) lines.push(`- Favorite Integer: ${userData.integer}`); + if (userData.number !== undefined) lines.push(`- Favorite Number: ${userData.number}`); + if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`); + + content.push({ + type: "text", + text: `User inputs:\n${lines.join('\n')}`, + }); + } else if (elicitationResult.action === 'decline') { + content.push({ + type: "text", + text: `❌ User declined to provide the requested information.`, + }); + } else if (elicitationResult.action === 'cancel') { + content.push({ + type: "text", + text: `⚠️ User cancelled the elicitation dialog.`, + }); + } + + // Include raw result for debugging + content.push({ + type: "text", + text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`, + }); + + return { content }; + } + }); + } }; const cleanup = async () => { diff --git a/src/everything/package.json b/src/everything/package.json index ca5964f9..f476cd3d 100644 --- a/src/everything/package.json +++ b/src/everything/package.json @@ -22,7 +22,7 @@ "start:streamableHttp": "node dist/streamableHttp.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "cors": "^2.8.5", "express": "^4.21.1", "jszip": "^3.10.1", diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 78881962..adb025bf 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -3,8 +3,6 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { - CallToolRequestSchema, - ListToolsRequestSchema, ToolSchema, RootsListChangedNotificationSchema, type Root, @@ -178,464 +176,470 @@ async function readFileAsBase64Stream(filePath: string): Promise { } // Tool handlers -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "read_file", - description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.", - inputSchema: zodToJsonSchema(ReadTextFileArgsSchema) as ToolInput, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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, - }, - { - 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({ + 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) => { + const parsed = ReadTextFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for read_file: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + + if (parsed.data.head && parsed.data.tail) { + throw new Error("Cannot specify both head and tail parameters simultaneously"); + } + + if (parsed.data.tail) { + // Use memory-efficient tail implementation for large files + const tailContent = await tailFile(validPath, parsed.data.tail); + return { + content: [{ type: "text", text: tailContent }], + }; + } + + if (parsed.data.head) { + // Use memory-efficient head implementation for large files + const headContent = await headFile(validPath, parsed.data.head); + return { + content: [{ type: "text", text: headContent }], + }; + } + const content = await readFileContent(validPath); + return { + content: [{ type: "text", text: content }], + }; + } }); - -server.setRequestHandler(CallToolRequestSchema, async (request) => { - try { - const { name, arguments: args } = request.params; - - switch (name) { - case "read_file": - case "read_text_file": { - const parsed = ReadTextFileArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for read_text_file: ${parsed.error}`); - } - const validPath = await validatePath(parsed.data.path); - - if (parsed.data.head && parsed.data.tail) { - throw new Error("Cannot specify both head and tail parameters simultaneously"); - } - - if (parsed.data.tail) { - // Use memory-efficient tail implementation for large files - const tailContent = await tailFile(validPath, parsed.data.tail); - return { - content: [{ type: "text", text: tailContent }], - }; - } - - if (parsed.data.head) { - // Use memory-efficient head implementation for large files - const headContent = await headFile(validPath, parsed.data.head); - return { - content: [{ type: "text", text: headContent }], - }; - } - const content = await readFileContent(validPath); - return { - content: [{ type: "text", text: content }], - }; - } - - case "read_media_file": { - const parsed = ReadMediaFileArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for read_media_file: ${parsed.error}`); - } - const validPath = await validatePath(parsed.data.path); - const extension = path.extname(validPath).toLowerCase(); - const mimeTypes: Record = { - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".webp": "image/webp", - ".bmp": "image/bmp", - ".svg": "image/svg+xml", - ".mp3": "audio/mpeg", - ".wav": "audio/wav", - ".ogg": "audio/ogg", - ".flac": "audio/flac", - }; - const mimeType = mimeTypes[extension] || "application/octet-stream"; - const data = await readFileAsBase64Stream(validPath); - const type = mimeType.startsWith("image/") - ? "image" - : mimeType.startsWith("audio/") - ? "audio" - : "blob"; - return { - content: [{ type, data, mimeType }], - }; - } - - case "read_multiple_files": { - const parsed = ReadMultipleFilesArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`); - } - const results = await Promise.all( - parsed.data.paths.map(async (filePath: string) => { - try { - const validPath = await validatePath(filePath); - const content = await readFileContent(validPath); - return `${filePath}:\n${content}\n`; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return `${filePath}: Error - ${errorMessage}`; - } - }), - ); - return { - content: [{ type: "text", text: results.join("\n---\n") }], - }; - } - - case "write_file": { - const parsed = WriteFileArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for write_file: ${parsed.error}`); - } - const validPath = await validatePath(parsed.data.path); - await writeFileContent(validPath, parsed.data.content); - return { - content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }], - }; - } - - case "edit_file": { - const parsed = EditFileArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for edit_file: ${parsed.error}`); - } - const validPath = await validatePath(parsed.data.path); - const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun); - return { - content: [{ type: "text", text: result }], - }; - } - - case "create_directory": { - const parsed = CreateDirectoryArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for create_directory: ${parsed.error}`); - } - const validPath = await validatePath(parsed.data.path); - await fs.mkdir(validPath, { recursive: true }); - return { - content: [{ type: "text", text: `Successfully created directory ${parsed.data.path}` }], - }; - } - - case "list_directory": { - const parsed = ListDirectoryArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for list_directory: ${parsed.error}`); - } - const validPath = await validatePath(parsed.data.path); - const entries = await fs.readdir(validPath, { withFileTypes: true }); - const formatted = entries - .map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`) - .join("\n"); - return { - content: [{ type: "text", text: formatted }], - }; - } - - case "list_directory_with_sizes": { - const parsed = ListDirectoryWithSizesArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for list_directory_with_sizes: ${parsed.error}`); - } - const validPath = await validatePath(parsed.data.path); - const entries = await fs.readdir(validPath, { withFileTypes: true }); - - // Get detailed information for each entry - const detailedEntries = await Promise.all( - entries.map(async (entry) => { - const entryPath = path.join(validPath, entry.name); - try { - const stats = await fs.stat(entryPath); - return { - name: entry.name, - isDirectory: entry.isDirectory(), - size: stats.size, - mtime: stats.mtime - }; - } catch (error) { - return { - name: entry.name, - isDirectory: entry.isDirectory(), - size: 0, - mtime: new Date(0) - }; - } - }) - ); - - // Sort entries based on sortBy parameter - const sortedEntries = [...detailedEntries].sort((a, b) => { - if (parsed.data.sortBy === 'size') { - return b.size - a.size; // Descending by size - } - // Default sort by name - return a.name.localeCompare(b.name); - }); - - // Format the output - const formattedEntries = sortedEntries.map(entry => - `${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${ - entry.isDirectory ? "" : formatSize(entry.size).padStart(10) - }` - ); - - // Add summary - const totalFiles = detailedEntries.filter(e => !e.isDirectory).length; - const totalDirs = detailedEntries.filter(e => e.isDirectory).length; - const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0); - - const summary = [ - "", - `Total: ${totalFiles} files, ${totalDirs} directories`, - `Combined size: ${formatSize(totalSize)}` - ]; - - return { - content: [{ - type: "text", - text: [...formattedEntries, ...summary].join("\n") - }], - }; - } - - case "directory_tree": { - const parsed = DirectoryTreeArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`); - } - - interface TreeEntry { - name: string; - type: 'file' | 'directory'; - children?: TreeEntry[]; - } - const rootPath = parsed.data.path; - - async function buildTree(currentPath: string, excludePatterns: string[] = []): Promise { - const validPath = await validatePath(currentPath); - const entries = await fs.readdir(validPath, {withFileTypes: true}); - const result: TreeEntry[] = []; - - for (const entry of entries) { - const relativePath = path.relative(rootPath, path.join(currentPath, entry.name)); - const shouldExclude = excludePatterns.some(pattern => { - if (pattern.includes('*')) { - return minimatch(relativePath, pattern, {dot: true}); - } - // For files: match exact name or as part of path - // For directories: match as directory path - return minimatch(relativePath, pattern, {dot: true}) || - minimatch(relativePath, `**/${pattern}`, {dot: true}) || - minimatch(relativePath, `**/${pattern}/**`, {dot: true}); - }); - if (shouldExclude) - continue; - - const entryData: TreeEntry = { - name: entry.name, - type: entry.isDirectory() ? 'directory' : 'file' - }; - - if (entry.isDirectory()) { - const subPath = path.join(currentPath, entry.name); - entryData.children = await buildTree(subPath, excludePatterns); - } - - result.push(entryData); - } - - return result; - } - - const treeData = await buildTree(rootPath, parsed.data.excludePatterns); - return { - content: [{ - type: "text", - text: JSON.stringify(treeData, null, 2) - }], - }; - } - - case "move_file": { - const parsed = MoveFileArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for move_file: ${parsed.error}`); - } - const validSourcePath = await validatePath(parsed.data.source); - const validDestPath = await validatePath(parsed.data.destination); - await fs.rename(validSourcePath, validDestPath); - return { - content: [{ type: "text", text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }], - }; - } - - case "search_files": { - const parsed = SearchFilesArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for search_files: ${parsed.error}`); - } - const validPath = await validatePath(parsed.data.path); - const results = await searchFilesWithValidation(validPath, parsed.data.pattern, allowedDirectories, { excludePatterns: parsed.data.excludePatterns }); - return { - content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }], - }; - } - - case "get_file_info": { - const parsed = GetFileInfoArgsSchema.safeParse(args); - if (!parsed.success) { - throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`); - } - const validPath = await validatePath(parsed.data.path); - const info = await getFileStats(validPath); - return { - content: [{ type: "text", text: Object.entries(info) - .map(([key, value]) => `${key}: ${value}`) - .join("\n") }], - }; - } - - case "list_allowed_directories": { - return { - content: [{ - type: "text", - text: `Allowed directories:\n${allowedDirectories.join('\n')}` - }], - }; - } - - default: - throw new Error(`Unknown tool: ${name}`); +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) => { + const parsed = ReadTextFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for read_text_file: ${parsed.error}`); } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + const validPath = await validatePath(parsed.data.path); + + if (parsed.data.head && parsed.data.tail) { + throw new Error("Cannot specify both head and tail parameters simultaneously"); + } + + if (parsed.data.tail) { + // Use memory-efficient tail implementation for large files + const tailContent = await tailFile(validPath, parsed.data.tail); + return { + content: [{ type: "text", text: tailContent }], + }; + } + + if (parsed.data.head) { + // Use memory-efficient head implementation for large files + const headContent = await headFile(validPath, parsed.data.head); + return { + content: [{ type: "text", text: headContent }], + }; + } + const content = await readFileContent(validPath); return { - content: [{ type: "text", text: `Error: ${errorMessage}` }], - isError: true, + 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) => { + const parsed = ReadMediaFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for read_media_file: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const extension = path.extname(validPath).toLowerCase(); + const mimeTypes: Record = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".bmp": "image/bmp", + ".svg": "image/svg+xml", + ".mp3": "audio/mpeg", + ".wav": "audio/wav", + ".ogg": "audio/ogg", + ".flac": "audio/flac", + }; + const mimeType = mimeTypes[extension] || "application/octet-stream"; + const data = await readFileAsBase64Stream(validPath); + const type = mimeType.startsWith("image/") + ? "image" + : mimeType.startsWith("audio/") + ? "audio" + : "blob"; + return { + 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) => { + const parsed = ReadMultipleFilesArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`); + } + const results = await Promise.all( + parsed.data.paths.map(async (filePath: string) => { + try { + const validPath = await validatePath(filePath); + const content = await readFileContent(validPath); + return `${filePath}:\n${content}\n`; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return `${filePath}: Error - ${errorMessage}`; + } + }), + ); + return { + 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) => { + const parsed = WriteFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for write_file: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + await writeFileContent(validPath, parsed.data.content); + return { + 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) => { + const parsed = EditFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for edit_file: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun); + return { + 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) => { + const parsed = CreateDirectoryArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for create_directory: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + await fs.mkdir(validPath, { recursive: true }); + return { + 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) => { + const parsed = ListDirectoryArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for list_directory: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const entries = await fs.readdir(validPath, { withFileTypes: true }); + const formatted = entries + .map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`) + .join("\n"); + return { + 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) => { + const parsed = ListDirectoryWithSizesArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for list_directory_with_sizes: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const entries = await fs.readdir(validPath, { withFileTypes: true }); + + // Get detailed information for each entry + const detailedEntries = await Promise.all( + entries.map(async (entry) => { + const entryPath = path.join(validPath, entry.name); + try { + const stats = await fs.stat(entryPath); + return { + name: entry.name, + isDirectory: entry.isDirectory(), + size: stats.size, + mtime: stats.mtime + }; + } catch (error) { + return { + name: entry.name, + isDirectory: entry.isDirectory(), + size: 0, + mtime: new Date(0) + }; + } + }) + ); + + // Sort entries based on sortBy parameter + const sortedEntries = [...detailedEntries].sort((a, b) => { + if (parsed.data.sortBy === 'size') { + return b.size - a.size; // Descending by size + } + // Default sort by name + return a.name.localeCompare(b.name); + }); + + // Format the output + const formattedEntries = sortedEntries.map(entry => + `${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${ + entry.isDirectory ? "" : formatSize(entry.size).padStart(10) + }` + ); + + // Add summary + const totalFiles = detailedEntries.filter(e => !e.isDirectory).length; + const totalDirs = detailedEntries.filter(e => e.isDirectory).length; + const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0); + + const summary = [ + "", + `Total: ${totalFiles} files, ${totalDirs} directories`, + `Combined size: ${formatSize(totalSize)}` + ]; + + return { + content: [{ + type: "text", + text: [...formattedEntries, ...summary].join("\n") + }], + }; + } +}); + +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) => { + const parsed = DirectoryTreeArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`); + } + + interface TreeEntry { + name: string; + type: 'file' | 'directory'; + children?: TreeEntry[]; + } + const rootPath = parsed.data.path; + + async function buildTree(currentPath: string, excludePatterns: string[] = []): Promise { + const validPath = await validatePath(currentPath); + const entries = await fs.readdir(validPath, {withFileTypes: true}); + const result: TreeEntry[] = []; + + for (const entry of entries) { + const relativePath = path.relative(rootPath, path.join(currentPath, entry.name)); + const shouldExclude = excludePatterns.some(pattern => { + if (pattern.includes('*')) { + return minimatch(relativePath, pattern, {dot: true}); + } + // For files: match exact name or as part of path + // For directories: match as directory path + return minimatch(relativePath, pattern, {dot: true}) || + minimatch(relativePath, `**/${pattern}`, {dot: true}) || + minimatch(relativePath, `**/${pattern}/**`, {dot: true}); + }); + if (shouldExclude) + continue; + + const entryData: TreeEntry = { + name: entry.name, + type: entry.isDirectory() ? 'directory' : 'file' + }; + + if (entry.isDirectory()) { + const subPath = path.join(currentPath, entry.name); + entryData.children = await buildTree(subPath, excludePatterns); + } + + result.push(entryData); + } + + return result; + } + + const treeData = await buildTree(rootPath, parsed.data.excludePatterns); + return { + content: [{ + type: "text", + text: JSON.stringify(treeData, null, 2) + }], + }; + } +}); + +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) => { + const parsed = MoveFileArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for move_file: ${parsed.error}`); + } + const validSourcePath = await validatePath(parsed.data.source); + const validDestPath = await validatePath(parsed.data.destination); + await fs.rename(validSourcePath, validDestPath); + return { + 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) => { + const parsed = SearchFilesArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for search_files: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const results = await searchFilesWithValidation(validPath, parsed.data.pattern, allowedDirectories, { excludePatterns: parsed.data.excludePatterns }); + return { + 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) => { + const parsed = GetFileInfoArgsSchema.safeParse(args); + if (!parsed.success) { + throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`); + } + const validPath = await validatePath(parsed.data.path); + const info = await getFileStats(validPath); + return { + content: [{ type: "text", text: Object.entries(info) + .map(([key, value]) => `${key}: ${value}`) + .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: [], + }, + handler: async (args: any) => { + return { + content: [{ + type: "text", + text: `Allowed directories:\n${allowedDirectories.join('\n')}` + }], }; } }); diff --git a/src/filesystem/package.json b/src/filesystem/package.json index faeefa54..c26ba40d 100644 --- a/src/filesystem/package.json +++ b/src/filesystem/package.json @@ -20,7 +20,7 @@ "test": "jest --config=jest.config.cjs --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.20.1", "diff": "^5.1.0", "glob": "^10.3.10", "minimatch": "^10.0.1", diff --git a/src/memory/index.ts b/src/memory/index.ts index 982c617b..db53d34b 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -2,10 +2,6 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -206,230 +202,226 @@ const server = new Server({ }, },); -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "create_entities", - description: "Create multiple new entities in the knowledge graph", - inputSchema: { - type: "object", - properties: { - entities: { - type: "array", - items: { - type: "object", - properties: { - name: { type: "string", description: "The name of the entity" }, - entityType: { type: "string", description: "The type of the entity" }, - observations: { - type: "array", - items: { type: "string" }, - description: "An array of observation contents associated with the entity" - }, - }, - required: ["name", "entityType", "observations"], - additionalProperties: false, - }, - }, - }, - required: ["entities"], - additionalProperties: false, - }, - }, - { - name: "create_relations", - description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice", - inputSchema: { - type: "object", - properties: { - relations: { - type: "array", - items: { - type: "object", - properties: { - from: { type: "string", description: "The name of the entity where the relation starts" }, - to: { type: "string", description: "The name of the entity where the relation ends" }, - relationType: { type: "string", description: "The type of the relation" }, - }, - required: ["from", "to", "relationType"], - additionalProperties: false, - }, - }, - }, - required: ["relations"], - additionalProperties: false, - }, - }, - { - name: "add_observations", - description: "Add new observations to existing entities in the knowledge graph", - inputSchema: { +server.registerTool({ + name: "create_entities", + description: "Create multiple new entities in the knowledge graph", + inputSchema: { + type: "object", + properties: { + entities: { + type: "array", + items: { type: "object", properties: { + name: { type: "string", description: "The name of the entity" }, + entityType: { type: "string", description: "The type of the entity" }, observations: { type: "array", - items: { - type: "object", - properties: { - entityName: { type: "string", description: "The name of the entity to add the observations to" }, - contents: { - type: "array", - items: { type: "string" }, - description: "An array of observation contents to add" - }, - }, - required: ["entityName", "contents"], - additionalProperties: false, - }, - }, - }, - required: ["observations"], - additionalProperties: false, - }, - }, - { - name: "delete_entities", - description: "Delete multiple entities and their associated relations from the knowledge graph", - inputSchema: { - type: "object", - properties: { - entityNames: { - type: "array", items: { type: "string" }, - description: "An array of entity names to delete" + description: "An array of observation contents associated with the entity" }, }, - required: ["entityNames"], + required: ["name", "entityType", "observations"], additionalProperties: false, }, }, - { - name: "delete_observations", - description: "Delete specific observations from entities in the knowledge graph", - inputSchema: { - type: "object", - properties: { - deletions: { - type: "array", - items: { - type: "object", - properties: { - entityName: { type: "string", description: "The name of the entity containing the observations" }, - observations: { - type: "array", - items: { type: "string" }, - description: "An array of observations to delete" - }, - }, - required: ["entityName", "observations"], - additionalProperties: false, - }, - }, - }, - required: ["deletions"], - additionalProperties: false, - }, - }, - { - name: "delete_relations", - description: "Delete multiple relations from the knowledge graph", - inputSchema: { - type: "object", - properties: { - relations: { - type: "array", - items: { - type: "object", - properties: { - from: { type: "string", description: "The name of the entity where the relation starts" }, - to: { type: "string", description: "The name of the entity where the relation ends" }, - relationType: { type: "string", description: "The type of the relation" }, - }, - required: ["from", "to", "relationType"], - additionalProperties: false, - }, - description: "An array of relations to delete" - }, - }, - required: ["relations"], - additionalProperties: false, - }, - }, - { - name: "read_graph", - description: "Read the entire knowledge graph", - inputSchema: { - type: "object", - properties: {}, - additionalProperties: false, - }, - }, - { - name: "search_nodes", - description: "Search for nodes in the knowledge graph based on a query", - inputSchema: { - type: "object", - properties: { - query: { type: "string", description: "The search query to match against entity names, types, and observation content" }, - }, - required: ["query"], - additionalProperties: false, - }, - }, - { - name: "open_nodes", - description: "Open specific nodes in the knowledge graph by their names", - inputSchema: { - type: "object", - properties: { - names: { - type: "array", - items: { type: "string" }, - description: "An array of entity names to retrieve", - }, - }, - required: ["names"], - additionalProperties: false, - }, - }, - ], - }; + }, + required: ["entities"], + additionalProperties: false, + }, + handler: async (args) => { + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; + } }); -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; +server.registerTool({ + name: "create_relations", + description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice", + inputSchema: { + type: "object", + properties: { + relations: { + type: "array", + items: { + type: "object", + properties: { + from: { type: "string", description: "The name of the entity where the relation starts" }, + to: { type: "string", description: "The name of the entity where the relation ends" }, + relationType: { type: "string", description: "The type of the relation" }, + }, + required: ["from", "to", "relationType"], + additionalProperties: false, + }, + }, + }, + required: ["relations"], + additionalProperties: false, + }, + handler: async (args) => { + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; + } +}); - if (name === "read_graph") { +server.registerTool({ + name: "add_observations", + description: "Add new observations to existing entities in the knowledge graph", + inputSchema: { + type: "object", + properties: { + observations: { + type: "array", + items: { + type: "object", + properties: { + entityName: { type: "string", description: "The name of the entity to add the observations to" }, + contents: { + type: "array", + items: { type: "string" }, + description: "An array of observation contents to add" + }, + }, + required: ["entityName", "contents"], + additionalProperties: false, + }, + }, + }, + required: ["observations"], + additionalProperties: false, + }, + handler: async (args) => { + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; + } +}); + +server.registerTool({ + name: "delete_entities", + description: "Delete multiple entities and their associated relations from the knowledge graph", + inputSchema: { + type: "object", + properties: { + entityNames: { + type: "array", + items: { type: "string" }, + description: "An array of entity names to delete" + }, + }, + required: ["entityNames"], + additionalProperties: false, + }, + handler: async (args) => { + await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); + return { content: [{ type: "text", text: "Entities deleted successfully" }] }; + } +}); + +server.registerTool({ + name: "delete_observations", + description: "Delete specific observations from entities in the knowledge graph", + inputSchema: { + type: "object", + properties: { + deletions: { + type: "array", + items: { + type: "object", + properties: { + entityName: { type: "string", description: "The name of the entity containing the observations" }, + observations: { + type: "array", + items: { type: "string" }, + description: "An array of observations to delete" + }, + }, + required: ["entityName", "observations"], + additionalProperties: false, + }, + }, + }, + required: ["deletions"], + additionalProperties: false, + }, + handler: async (args) => { + await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); + return { content: [{ type: "text", text: "Observations deleted successfully" }] }; + } +}); + +server.registerTool({ + name: "delete_relations", + description: "Delete multiple relations from the knowledge graph", + inputSchema: { + type: "object", + properties: { + relations: { + type: "array", + items: { + type: "object", + properties: { + from: { type: "string", description: "The name of the entity where the relation starts" }, + to: { type: "string", description: "The name of the entity where the relation ends" }, + relationType: { type: "string", description: "The type of the relation" }, + }, + required: ["from", "to", "relationType"], + additionalProperties: false, + }, + description: "An array of relations to delete" + }, + }, + required: ["relations"], + additionalProperties: false, + }, + handler: async (args) => { + await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); + return { content: [{ type: "text", text: "Relations deleted successfully" }] }; + } +}); + +server.registerTool({ + name: "read_graph", + description: "Read the entire knowledge graph", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + handler: async () => { return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.readGraph(), null, 2) }] }; } +}); - if (!args) { - throw new Error(`No arguments provided for tool: ${name}`); +server.registerTool({ + name: "search_nodes", + description: "Search for nodes in the knowledge graph based on a query", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "The search query to match against entity names, types, and observation content" }, + }, + required: ["query"], + additionalProperties: false, + }, + handler: async (args) => { + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.searchNodes(args.query as string), null, 2) }] }; } +}); - switch (name) { - case "create_entities": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities as Entity[]), null, 2) }] }; - case "create_relations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations as Relation[]), null, 2) }] }; - case "add_observations": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations as { entityName: string; contents: string[] }[]), null, 2) }] }; - case "delete_entities": - await knowledgeGraphManager.deleteEntities(args.entityNames as string[]); - return { content: [{ type: "text", text: "Entities deleted successfully" }] }; - case "delete_observations": - await knowledgeGraphManager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[]); - return { content: [{ type: "text", text: "Observations deleted successfully" }] }; - case "delete_relations": - await knowledgeGraphManager.deleteRelations(args.relations as Relation[]); - return { content: [{ type: "text", text: "Relations deleted successfully" }] }; - case "search_nodes": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.searchNodes(args.query as string), null, 2) }] }; - case "open_nodes": - return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.openNodes(args.names as string[]), null, 2) }] }; - default: - throw new Error(`Unknown tool: ${name}`); +server.registerTool({ + name: "open_nodes", + description: "Open specific nodes in the knowledge graph by their names", + inputSchema: { + type: "object", + properties: { + names: { + type: "array", + items: { type: "string" }, + description: "An array of entity names to retrieve", + }, + }, + required: ["names"], + additionalProperties: false, + }, + handler: async (args) => { + return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.openNodes(args.names as string[]), null, 2) }] }; } }); diff --git a/src/memory/package.json b/src/memory/package.json index b64cf3b6..ea7c9beb 100644 --- a/src/memory/package.json +++ b/src/memory/package.json @@ -19,7 +19,7 @@ "watch": "tsc --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "1.0.1" + "@modelcontextprotocol/sdk": "^1.20.1" }, "devDependencies": { "@types/node": "^22", diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index 34986971..e0682219 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -3,8 +3,6 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { - CallToolRequestSchema, - ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; // Fixed chalk import for ESM @@ -255,22 +253,13 @@ const server = new Server( const thinkingServer = new SequentialThinkingServer(); -server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: [SEQUENTIAL_THINKING_TOOL], -})); - -server.setRequestHandler(CallToolRequestSchema, async (request) => { - if (request.params.name === "sequentialthinking") { - return thinkingServer.processThought(request.params.arguments); +server.registerTool({ + name: SEQUENTIAL_THINKING_TOOL.name, + description: SEQUENTIAL_THINKING_TOOL.description, + inputSchema: SEQUENTIAL_THINKING_TOOL.inputSchema, + handler: async (args) => { + return thinkingServer.processThought(args); } - - return { - content: [{ - type: "text", - text: `Unknown tool: ${request.params.name}` - }], - isError: true - }; }); async function runServer() { diff --git a/src/sequentialthinking/package.json b/src/sequentialthinking/package.json index 89205d30..920dc62f 100644 --- a/src/sequentialthinking/package.json +++ b/src/sequentialthinking/package.json @@ -19,7 +19,7 @@ "watch": "tsc --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "0.5.0", + "@modelcontextprotocol/sdk": "^1.20.1", "chalk": "^5.3.0", "yargs": "^17.7.2" },