Skip to content

Implement core MCP tools for file operation#24

Merged
Patrick-Ehimen merged 1 commit intomainfrom
feat/mcp-file-operations-tools
Oct 18, 2025
Merged

Implement core MCP tools for file operation#24
Patrick-Ehimen merged 1 commit intomainfrom
feat/mcp-file-operations-tools

Conversation

@Patrick-Ehimen
Copy link
Copy Markdown
Owner

@Patrick-Ehimen Patrick-Ehimen commented Oct 16, 2025

Summary

Implemented core MCP tools for file upload and download operations, integrating with the unified SDK wrapper and providing comprehensive parameter validation, progress tracking, and error handling.

Features Implemented

🔧 lighthouse_upload_file Tool

  • Parameter validation: filePath (required), encrypt, accessConditions, tags
  • File validation: Existence, type, and size checks (100MB limit)
  • Security: Access conditions validation requires encryption
  • Integration: Uses unified LighthouseService wrapper
  • Progress tracking: Execution time monitoring and metadata
  • Error handling: Comprehensive validation and service error handling

📥 lighthouse_fetch_file Tool

  • CID validation: Supports both CID v0 (46 chars) and v1 formats
  • Path validation: Output directory creation and file existence checks
  • Decryption support: Optional file decryption during download
  • Integration: Uses unified LighthouseService wrapper
  • Progress tracking: Download progress and execution metadata
  • Error handling: CID format, filesystem, and service errors

🧪 Comprehensive Testing

  • Upload tool: 15 test cases covering success, validation, and error scenarios
  • Download tool: 18 test cases covering success, validation, and service errors
  • Coverage: Parameter validation, file system operations, service integration
  • Test framework: Vitest with proper mocking of filesystem and service calls

Requirements Fulfilled

✅ MCP tools integrated with unified SDK wrapper
✅ File download functionality implemented
✅ SDK wrapper integration for actual Lighthouse operations
✅ Upload file tool with parameter validation
✅ Download functionality with streaming support

Testing Results

  • Total tests: 154 tests across the project
  • Passing: 151 tests (98% pass rate)
  • New test coverage: 33 tests for new MCP tools
  • Build status: ✅ TypeScript compilation successful

Usage Examples

Upload File

{
  "method": "tools/call", 
  "params": {
    "name": "lighthouse_upload_file",
    "arguments": {
      "filePath": "/path/to/file.txt",
      "encrypt": true,
      "tags": ["document", "important"]
    }
  }
}

Download File

{
  "method": "tools/call",
  "params": {
    "name": "lighthouse_fetch_file", 
    "arguments": {
      "cid": "QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "outputPath": "./downloads/file.txt",
      "decrypt": true
    }
  }
}

Checklist

  • Implementation follows existing codebase patterns
  • Comprehensive unit tests with high coverage
  • TypeScript compilation successful
  • Integration with existing service layer
  • Parameter validation and error handling
  • Progress tracking and metadata collection
  • Documentation and code comments
  • Short, simple, and concise implementation as requested

Summary by Sourcery

Implement and integrate core MCP tools for file upload and download operations using the unified LighthouseService wrapper, adding comprehensive parameter validation, progress tracking, and error handling.

New Features:

  • Add lighthouse_upload_file tool with encryption, access control, tag support, file validation, progress tracking, and error handling
  • Add lighthouse_fetch_file tool with CID v0/v1 validation, output path management, optional decryption, progress tracking, and error handling

Enhancements:

  • Refactor server.ts to register tools via LighthouseUploadFileTool and LighthouseFetchFileTool classes and dynamic definitions
  • Introduce central tools index and shared type definitions for MCP tools

Documentation:

  • Update integration documentation to reflect real SDK wrapper usage

Tests:

  • Add Vitest test suites for upload and fetch tools covering success scenarios, input validation errors, service errors, and metadata tracking

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Oct 16, 2025

Reviewer's Guide

This PR introduces two MCP tools for file upload and download—LighthouseUploadFileTool and LighthouseFetchFileTool—complete with reusable definitions, parameter validation, progress tracking, error handling, and integration into the MCP server registry; it refactors server registration to leverage these tool classes, adds shared types and exports, and updates documentation accordingly.

Sequence diagram for MCP server tool registration (refactored)

sequenceDiagram
    participant Server as "LighthouseMCPServer"
    participant Registry
    participant UploadTool as "LighthouseUploadFileTool"
    participant FetchTool as "LighthouseFetchFileTool"
    Server->>UploadTool: new LighthouseUploadFileTool(service, logger)
    Server->>FetchTool: new LighthouseFetchFileTool(service, logger)
    Server->>Registry: register(UploadTool.getDefinition(), UploadTool.execute)
    Server->>Registry: register(FetchTool.getDefinition(), FetchTool.execute)
    Server->>Registry: register(datasetTool, datasetService.createDataset)
    Registry->>Server: listTools()
    Server->>Server: log toolCount, toolNames
Loading

Class diagram for new MCP file tools

classDiagram
    class LighthouseUploadFileTool {
        - ILighthouseService service
        - Logger logger
        + constructor(service: ILighthouseService, logger?: Logger)
        + static getDefinition(): MCPToolDefinition
        - validateParams(params: UploadFileParams): Promise<string | null>
        + execute(args: Record<string, unknown>): Promise<ProgressAwareToolResult>
    }
    class LighthouseFetchFileTool {
        - ILighthouseService service
        - Logger logger
        + constructor(service: ILighthouseService, logger?: Logger)
        + static getDefinition(): MCPToolDefinition
        - isValidCID(cid: string): boolean
        - validateParams(params: FetchFileParams): Promise<string | null>
        + execute(args: Record<string, unknown>): Promise<ProgressAwareToolResult>
    }
    class ILighthouseService {
        <<interface>>
        + uploadFile(params)
        + fetchFile(params)
        + getFileInfo(cid)
    }
    class Logger {
        + info(...)
        + warn(...)
        + error(...)
        + getInstance(...)
    }
    class ProgressAwareToolResult {
        + success: boolean
        + data?: unknown
        + error?: string
        + metadata?: Record<string, unknown>
        + operationId?: string
        + isRunning?: boolean
        + executionTime: number
    }
    LighthouseUploadFileTool --> ILighthouseService
    LighthouseUploadFileTool --> Logger
    LighthouseFetchFileTool --> ILighthouseService
    LighthouseFetchFileTool --> Logger
    LighthouseUploadFileTool ..> ProgressAwareToolResult
    LighthouseFetchFileTool ..> ProgressAwareToolResult
    ILighthouseService <|.. LighthouseUploadFileTool
    ILighthouseService <|.. LighthouseFetchFileTool
Loading

File-Level Changes

Change Details Files
Refactor server to use tool classes for registration
  • Instantiate upload and fetch tool classes with service & logger
  • Register tools via getDefinition() and execute() calls
  • Adjust registry logging to reflect dynamic tool list
apps/mcp-server/src/server.ts
Implement LighthouseUploadFileTool with validation and execution logic
  • Define tool metadata and input schema
  • Validate file existence, size, encryption, access conditions, tags
  • Execute upload via unified LighthouseService wrapper with metadata tracking
  • Handle errors and format response with progress metadata
apps/mcp-server/src/tools/LighthouseUploadFileTool.ts
apps/mcp-server/src/tools/__tests__/LighthouseUploadFileTool.test.ts
Implement LighthouseFetchFileTool with validation and execution logic
  • Define tool metadata and input schema
  • Validate CID formats, output paths, directory and file existence, decryption flag
  • Execute download via unified LighthouseService wrapper with metadata tracking
  • Handle errors and format response with progress metadata
apps/mcp-server/src/tools/LighthouseFetchFileTool.ts
apps/mcp-server/src/tools/__tests__/LighthouseFetchFileTool.test.ts
Add shared types and tool exports
  • Define ProgressAwareToolResult, ToolResult, ToolProgressEvent in types.ts
  • Export tools and factories in index.ts
apps/mcp-server/src/tools/types.ts
apps/mcp-server/src/tools/index.ts
Update integration documentation
  • Simplify INTEGRATION.md description
  • Clarify real LighthouseService integration with unified SDK wrapper
apps/mcp-server/INTEGRATION.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `apps/mcp-server/src/server.ts:138-147` </location>
<code_context>
+    if (datasetTool) {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Silent fallback if dataset tool is missing may obscure configuration errors.

Consider adding a warning log when the tool is missing to improve visibility and make debugging configuration issues easier.
</issue_to_address>

### Comment 2
<location> `apps/mcp-server/src/server.ts:160` </location>
<code_context>
+
     this.logger.info("All tools registered", {
-      toolCount: LIGHTHOUSE_MCP_TOOLS.length,
+      toolCount: registeredTools.length,
+      toolNames: registeredTools.map((t) => t.name),
       registrationTime,
</code_context>

<issue_to_address>
**suggestion:** Switching toolCount to registeredTools.length may not reflect intended tool coverage.

Reporting only registeredTools.length may misrepresent tool coverage if some tools fail to register. Consider including both the total available and registered tool counts, or update the log message for clarity.
</issue_to_address>

### Comment 3
<location> `apps/mcp-server/src/tools/LighthouseFetchFileTool.ts:72-81` </location>
<code_context>
+  private isValidCID(cid: string): boolean {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** CID validation logic may reject valid CIDs or allow invalid ones.

Consider using a library like 'multiformats/cid' for CID validation to ensure compatibility with all valid formats and reduce the risk of incorrect validation.

Suggested implementation:

```typescript
import { CID } from "multiformats/cid";

  /**
   * Validate CID format using multiformats/cid library
   */
  private isValidCID(cid: string): boolean {
    if (typeof cid !== "string" || cid.length === 0) return false;
    try {
      CID.parse(cid);
      return true;
    } catch {
      return false;
    }
  }

```

Make sure to add `multiformats` to your dependencies if it is not already installed:
```
npm install multiformats
```
Remove any unused imports or code related to the old validation logic if present elsewhere in the file.
</issue_to_address>

### Comment 4
<location> `apps/mcp-server/src/tools/__tests__/LighthouseUploadFileTool.test.ts:179-192` </location>
<code_context>
+      expect(result.error).toContain("Path is not a file");
+    });
+
+    it("should fail when file is too large", async () => {
+      mockFs.stat.mockResolvedValue({
+        isFile: () => true,
+        size: 200 * 1024 * 1024, // 200MB
+      } as any);
+
+      const result = await tool.execute({
+        filePath: "/test/huge-file.txt",
+      });
+
+      expect(result.success).toBe(false);
+      expect(result.error).toContain("File too large");
+    });
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test for a file exactly at the size limit (100MB).

Please add a test for a file with a size of exactly 100MB to verify correct handling of the boundary condition.

```suggestion
+    it("should succeed when file is exactly at the size limit (100MB)", async () => {
+      mockFs.stat.mockResolvedValue({
+        isFile: () => true,
+        size: 100 * 1024 * 1024, // 100MB
+      } as any);
+
+      const result = await tool.execute({
+        filePath: "/test/exact-limit-file.txt",
+      });
+
+      expect(result.success).toBe(true);
+      expect(result.error).toBeUndefined();
+    });
+
+    it("should fail when file is too large", async () => {
+      mockFs.stat.mockResolvedValue({
+        isFile: () => true,
+        size: 200 * 1024 * 1024, // 200MB
+      } as any);
+
+      const result = await tool.execute({
+        filePath: "/test/huge-file.txt",
+      });
+
+      expect(result.success).toBe(false);
+      expect(result.error).toContain("File too large");
+    });
+
```
</issue_to_address>

### Comment 5
<location> `apps/mcp-server/src/tools/__tests__/LighthouseUploadFileTool.test.ts:204-206` </location>
<code_context>
+      expect(result.error).toContain("encrypt must be a boolean");
+    });
+
+    it("should fail when access conditions are provided without encryption", async () => {
+      mockFs.stat.mockResolvedValue({
+        isFile: () => true,
+        size: 1024,
+      } as any);
+
+      const result = await tool.execute({
+        filePath: "/test/file.txt",
+        encrypt: false,
+        accessConditions: [
+          {
+            type: "token_balance",
+            condition: ">=",
+            value: "1000",
+          },
+        ],
+      });
+
+      expect(result.success).toBe(false);
+      expect(result.error).toContain("Access conditions require encryption");
+    });
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test for accessConditions with encrypt set to undefined.

Please add a test case where accessConditions are set but encrypt is undefined, to verify that the validation logic enforces encryption as required.

```suggestion
      expect(result.success).toBe(false);
      expect(result.error).toContain("encrypt must be a boolean");
    });

    it("should fail when access conditions are provided and encrypt is undefined", async () => {
      mockFs.stat.mockResolvedValue({
        isFile: () => true,
        size: 1024,
      } as any);

      const result = await tool.execute({
        filePath: "/test/file.txt",
        // encrypt is intentionally omitted (undefined)
        accessConditions: [
          {
            type: "token_balance",
            condition: ">=",
            value: "1000",
          },
        ],
      });

      expect(result.success).toBe(false);
      expect(result.error).toContain("Access conditions require encryption");
    });
```
</issue_to_address>

### Comment 6
<location> `apps/mcp-server/src/tools/__tests__/LighthouseFetchFileTool.test.ts:81-90` </location>
<code_context>
+    it("should download file successfully with minimal parameters", async () => {
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a test for outputPath pointing to a directory instead of a file.

Testing this scenario will help verify that the tool behaves as expected when given a directory path for outputPath.
</issue_to_address>

### Comment 7
<location> `apps/mcp-server/src/tools/__tests__/LighthouseFetchFileTool.test.ts:263-270` </location>
<code_context>
+      expect(result.error).toContain("Output file already exists");
+    });
+
+    it("should fail when decrypt is not boolean", async () => {
+      const result = await tool.execute({
+        cid: "QmTestCID123456789012345678901234567890123456",
+        decrypt: "yes",
+      });
+
+      expect(result.success).toBe(false);
+      expect(result.error).toContain("decrypt must be a boolean");
+    });
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test for decrypt being undefined when downloading an encrypted file.

Add a test case where decrypt is undefined for an encrypted file to verify correct default behavior.

Suggested implementation:

```typescript
      expect(result.success).toBe(false);
      expect(result.error).toContain("Output file already exists");
    });

    it("should handle decrypt being undefined for an encrypted file", async () => {
      // Mock the tool to simulate an encrypted file
      vi.spyOn(tool, "isFileEncrypted").mockReturnValue(true);

      const result = await tool.execute({
        cid: "QmTestCID123456789012345678901234567890123456",
        // decrypt is intentionally undefined
      });

      // Adjust the expectation below to match the intended default behavior.
      // If the default is to decrypt, expect success and decrypted output.
      // If the default is to throw an error, expect failure and error message.
      // Example expectation for default decrypt:
      expect(result.success).toBe(true);
      expect(result.output).toContain("decrypted"); // Adjust as needed for your output
    });

```

- You may need to adjust the mock for `isFileEncrypted` and the expectations to match your actual implementation and default behavior.
- If your tool uses a different method to determine encryption, update the mock accordingly.
- If the default behavior is to throw an error when `decrypt` is undefined, change the expectation to `expect(result.success).toBe(false)` and check for the appropriate error message.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +138 to 147
if (datasetTool) {
this.registry.register(datasetTool, async (args) => {
const result = await this.datasetService.createDataset({
name: args.name as string,
description: args.description as string | undefined,
files: args.files as string[],
metadata: args.metadata as Record<string, unknown> | undefined,
encrypt: args.encrypt as boolean | undefined,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Silent fallback if dataset tool is missing may obscure configuration errors.

Consider adding a warning log when the tool is missing to improve visibility and make debugging configuration issues easier.


this.logger.info("All tools registered", {
toolCount: LIGHTHOUSE_MCP_TOOLS.length,
toolCount: registeredTools.length,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Switching toolCount to registeredTools.length may not reflect intended tool coverage.

Reporting only registeredTools.length may misrepresent tool coverage if some tools fail to register. Consider including both the total available and registered tool counts, or update the log message for clarity.

Comment on lines +72 to +81
private isValidCID(cid: string): boolean {
// Basic CID validation - should start with Qm (v0) or b (v1 base32) and have proper length
if (typeof cid !== "string" || cid.length === 0) return false;

// CID v0 (base58, starts with Qm, 46 characters) - strict validation
if (cid.startsWith("Qm") && cid.length === 46) {
return /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/.test(cid);
}

// CID v1 (multibase, various encodings)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): CID validation logic may reject valid CIDs or allow invalid ones.

Consider using a library like 'multiformats/cid' for CID validation to ensure compatibility with all valid formats and reduce the risk of incorrect validation.

Suggested implementation:

import { CID } from "multiformats/cid";

  /**
   * Validate CID format using multiformats/cid library
   */
  private isValidCID(cid: string): boolean {
    if (typeof cid !== "string" || cid.length === 0) return false;
    try {
      CID.parse(cid);
      return true;
    } catch {
      return false;
    }
  }

Make sure to add multiformats to your dependencies if it is not already installed:

npm install multiformats

Remove any unused imports or code related to the old validation logic if present elsewhere in the file.

Comment on lines +204 to +206
expect(result.success).toBe(false);
expect(result.error).toContain("encrypt must be a boolean");
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for accessConditions with encrypt set to undefined.

Please add a test case where accessConditions are set but encrypt is undefined, to verify that the validation logic enforces encryption as required.

Suggested change
expect(result.success).toBe(false);
expect(result.error).toContain("encrypt must be a boolean");
});
expect(result.success).toBe(false);
expect(result.error).toContain("encrypt must be a boolean");
});
it("should fail when access conditions are provided and encrypt is undefined", async () => {
mockFs.stat.mockResolvedValue({
isFile: () => true,
size: 1024,
} as any);
const result = await tool.execute({
filePath: "/test/file.txt",
// encrypt is intentionally omitted (undefined)
accessConditions: [
{
type: "token_balance",
condition: ">=",
value: "1000",
},
],
});
expect(result.success).toBe(false);
expect(result.error).toContain("Access conditions require encryption");
});

Comment on lines +81 to +90
it("should download file successfully with minimal parameters", async () => {
const mockResult: DownloadResult = {
filePath: "./downloaded_QmTestCID123456789012345678901234567890123456",
cid: "QmTestCID123456789012345678901234567890123456",
size: 1024,
decrypted: false,
downloadedAt: new Date("2023-01-01T00:00:00.000Z"),
hash: "QmTestCID123456789012345678901234567890123456",
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding a test for outputPath pointing to a directory instead of a file.

Testing this scenario will help verify that the tool behaves as expected when given a directory path for outputPath.

@Patrick-Ehimen Patrick-Ehimen merged commit 263f468 into main Oct 18, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant