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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ npm-debug.log*
.idea/
*.swp
*.swo
.windsurf/

# Operating System files
.DS_Store
Expand Down
18 changes: 18 additions & 0 deletions src/mcp-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { downloadText, DownloadTextArgs } from './tools/download-text';
import { uploadFile, UploadFileArgs } from './tools/upload-file';
import { uploadFolder, UploadFolderArgs } from './tools/upload-folder';
import { downloadFolder, DownloadFolderArgs } from './tools/download-folder';
import { queryUploadProgress, QueryUploadProgressArgs } from './tools/query-upload-progress';

/**
* Swarm MCP Server class
Expand Down Expand Up @@ -168,6 +169,20 @@ export class SwarmMCPServer {
required: ['reference'],
},
},
{
name: 'query_upload_progress',
description: 'Query upload progress for a specific upload session identified with the returned Tag ID',
inputSchema: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps we can also add output schema here.

type: 'object',
properties: {
tagId: {
type: 'string',
description: 'Tag ID returned by upload_file and upload_folder tools to track upload progress',
},
},
required: ['tagId'],
},
},
],
}));

Expand All @@ -191,6 +206,9 @@ export class SwarmMCPServer {

case 'download_folder':
return downloadFolder(args as unknown as DownloadFolderArgs, this.bee, this.server.server.transport);

case 'query_upload_progress':
return queryUploadProgress(args as unknown as QueryUploadProgressArgs, this.bee, this.server.server.transport);
}

throw new McpError(
Expand Down
84 changes: 84 additions & 0 deletions src/tools/query-upload-progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* MCP Tool: query_upload_progress
* Query upload progress for a specific upload session identified with the Tag ID
*/
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { Bee } from '@ethersphere/bee-js';
import { ToolResponse } from '../utils';

export interface QueryUploadProgressArgs {
tagId: string;
}

// The third argument (transport) is accepted for parity with other tools but unused here
export async function queryUploadProgress(
args: QueryUploadProgressArgs,
bee: Bee,
_transport?: unknown
): Promise<ToolResponse> {
if (!args?.tagId) {
throw new McpError(ErrorCode.InvalidParams, 'Missing required parameter: tagId');
}

const tagUid = Number.parseInt(args.tagId, 10);
if (Number.isNaN(tagUid)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid tagId format. Expected a numeric string.');
}

// TODO check tag endpoint availability - gateway mode

try {
const tag = await bee.retrieveTag(tagUid);

const synced = tag.synced ?? 0;
const seen = tag.seen ?? 0;
const processed = synced + seen;
const total = tag.split ?? 0;
const startedAt = tag.startedAt;

const processedPercentage = total > 0 ? Math.round((processed / total) * 100) : 0;
const isComplete = processedPercentage === 100;

let tagDeleted = false;
if (isComplete) {
try {
await bee.deleteTag(tagUid);
tagDeleted = true;
} catch {
// Non-fatal: if deletion fails we still return progress
}
}

return {
content: [
{
type: 'text',
text: JSON.stringify(
{
processedPercentage,
message: isComplete
? 'Upload completed successfully.'
: `Upload progress: ${processedPercentage}% processed`,
startedAt,
},
null,
2
),
},
],
};
} catch (error: any) {
const status = error?.status ?? error?.response?.status;
if (status === 404) {
throw new McpError(
ErrorCode.InvalidParams,
`Tag with ID ${args.tagId} does not exist or has been deleted`
);
}

throw new McpError(
ErrorCode.InternalError,
`Failed to retrieve upload progress: ${error?.message ?? 'Unknown error'}`
);
}
}
26 changes: 23 additions & 3 deletions src/tools/upload-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Upload a file to Swarm
*/
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { Bee } from '@ethersphere/bee-js';
import { Bee, FileUploadOptions } from '@ethersphere/bee-js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import fs from 'fs';
import { promisify } from 'util';
Expand Down Expand Up @@ -50,7 +50,26 @@ export async function uploadFile(args: UploadFileArgs, bee: Bee, transport: any)
binaryData = Buffer.from(args.data, 'base64');
}

const result = await bee.uploadFile(config.bee.postageBatchId, binaryData, name);
const redundancyLevel = args.redundancyLevel;
const options: FileUploadOptions = {};

// TODO check tag endpoint availability - gateway mode
const deferred = binaryData.length > 5 * 1024 * 1024;
options.deferred = deferred;
options.redundancyLevel = redundancyLevel;

let message = 'File successfully uploaded to Swarm';
let tagId: string | undefined = undefined;
// Create tag for deferred uploads or when explicitly requested
if (deferred) {
const tag = await bee.createTag();
options.tag = tag.uid;
tagId = tag.uid.toString();
message = 'File upload started in deferred mode. Use query_upload_progress to track progress.';
}

// Start the deferred upload
const result = await bee.uploadFile(config.bee.postageBatchId, binaryData, name, options);

return {
content: [
Expand All @@ -59,7 +78,8 @@ export async function uploadFile(args: UploadFileArgs, bee: Bee, transport: any)
text: JSON.stringify({
reference: result.reference.toString(),
url: config.bee.endpoint + '/bzz/' + result.reference.toString(),
message: 'File successfully uploaded to Swarm',
message,
tagId,
}, null, 2),
},
],
Expand Down
29 changes: 26 additions & 3 deletions src/tools/upload-folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Upload a folder to Swarm
*/
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { Bee } from '@ethersphere/bee-js';
import { Bee, CollectionUploadOptions } from '@ethersphere/bee-js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import fs from 'fs';
import { promisify } from 'util';
Expand Down Expand Up @@ -41,8 +41,30 @@ export async function uploadFolder(args: UploadFolderArgs, bee: Bee, transport:
}

const redundancyLevel = args.redundancyLevel;
const options = redundancyLevel ? { redundancyLevel } : undefined;
const options: CollectionUploadOptions = {};

if (redundancyLevel) {
options.redundancyLevel = redundancyLevel;
}

// TODO check tag endpoint availability - gateway mode
const deferred = true;
options.deferred = deferred;
let message = 'Folder successfully uploaded to Swarm';

let tagId: number | undefined = undefined;
if (deferred) {
try {
const tag = await bee.createTag();
tagId = tag.uid;
options.tag = tag.uid;
message = 'Folder upload started in deferred mode. Use query_upload_progress to track progress.';
} catch (error) {
console.error('Failed to create tag:', error);
options.deferred = false;
}
}

const result = await bee.uploadFilesFromDirectory(config.bee.postageBatchId, args.folderPath, options);

return {
Expand All @@ -52,7 +74,8 @@ export async function uploadFolder(args: UploadFolderArgs, bee: Bee, transport:
text: JSON.stringify({
reference: result.reference.toString(),
url: config.bee.endpoint + '/bzz/' + result.reference.toString(),
message: 'Folder successfully uploaded to Swarm',
message,
tagId,
}, null, 2),
},
],
Expand Down