feat: project files MCP tools — list, get, download, upload, update description, delete
This commit is contained in:
parent
5e60e34c3f
commit
e4e604b049
112
src/tools/files.ts
Normal file
112
src/tools/files.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||
import type { ToolContext } from './types.js';
|
||||
|
||||
const ListFilesParams = Type.Object({
|
||||
project_slug: Type.String({ description: 'Project slug' }),
|
||||
search: Type.Optional(Type.String({ description: 'Search by filename' })),
|
||||
});
|
||||
|
||||
const GetFileParams = Type.Object({
|
||||
project_slug: Type.String({ description: 'Project slug' }),
|
||||
file_id: Type.String({ description: 'File UUID' }),
|
||||
});
|
||||
|
||||
const DownloadFileParams = Type.Object({
|
||||
project_slug: Type.String({ description: 'Project slug' }),
|
||||
file_id: Type.String({ description: 'File UUID' }),
|
||||
});
|
||||
|
||||
const UploadFileParams = Type.Object({
|
||||
project_slug: Type.String({ description: 'Project slug' }),
|
||||
filename: Type.String({ description: 'File name (e.g. "spec.md")' }),
|
||||
content: Type.String({ description: 'File content as base64 string' }),
|
||||
description: Type.Optional(Type.String({ description: 'File description' })),
|
||||
});
|
||||
|
||||
const UpdateFileParams = Type.Object({
|
||||
project_slug: Type.String({ description: 'Project slug' }),
|
||||
file_id: Type.String({ description: 'File UUID' }),
|
||||
description: Type.String({ description: 'New description' }),
|
||||
});
|
||||
|
||||
const DeleteFileParams = Type.Object({
|
||||
project_slug: Type.String({ description: 'Project slug' }),
|
||||
file_id: Type.String({ description: 'File UUID' }),
|
||||
});
|
||||
|
||||
function ok(text: string) {
|
||||
return { content: [{ type: 'text' as const, text }], details: {} };
|
||||
}
|
||||
|
||||
export function createFileTools(ctx: ToolContext): ToolDefinition<any>[] {
|
||||
return [
|
||||
{
|
||||
name: 'list_project_files',
|
||||
label: 'List Project Files',
|
||||
description: 'List files in a project. Optionally search by filename.',
|
||||
parameters: ListFilesParams,
|
||||
async execute(_id: string, params: any) {
|
||||
const files = await ctx.trackerClient.listProjectFiles(params.project_slug, params.search);
|
||||
if (files.length === 0) return ok('No files found.');
|
||||
const list = files.map((f: any) => `- ${f.filename} (${f.size} bytes) ${f.description ? `— ${f.description}` : ''} [id: ${f.id}]`).join('\n');
|
||||
return ok(`${files.length} file(s):\n${list}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_project_file',
|
||||
label: 'Get Project File Info',
|
||||
description: 'Get metadata of a project file by ID.',
|
||||
parameters: GetFileParams,
|
||||
async execute(_id: string, params: any) {
|
||||
const file = await ctx.trackerClient.getProjectFile(params.project_slug, params.file_id);
|
||||
return ok(JSON.stringify(file, null, 2));
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'download_project_file',
|
||||
label: 'Download Project File',
|
||||
description: 'Download the content of a project file (returns text content).',
|
||||
parameters: DownloadFileParams,
|
||||
async execute(_id: string, params: any) {
|
||||
const content = await ctx.trackerClient.downloadProjectFile(params.project_slug, params.file_id);
|
||||
return ok(content);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'upload_project_file',
|
||||
label: 'Upload Project File',
|
||||
description: 'Upload a file to project. Content must be base64 encoded. If file with same name exists, it will be updated.',
|
||||
parameters: UploadFileParams,
|
||||
async execute(_id: string, params: any) {
|
||||
const result = await ctx.trackerClient.uploadProjectFile(
|
||||
params.project_slug,
|
||||
params.filename,
|
||||
params.content,
|
||||
params.description,
|
||||
);
|
||||
return ok(`Uploaded: ${(result as any).filename} (${(result as any).size} bytes)`);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'update_file_description',
|
||||
label: 'Update File Description',
|
||||
description: 'Update the description of a project file.',
|
||||
parameters: UpdateFileParams,
|
||||
async execute(_id: string, params: any) {
|
||||
await ctx.trackerClient.updateProjectFile(params.project_slug, params.file_id, { description: params.description });
|
||||
return ok('Description updated.');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete_project_file',
|
||||
label: 'Delete Project File',
|
||||
description: 'Delete a project file from disk and database.',
|
||||
parameters: DeleteFileParams,
|
||||
async execute(_id: string, params: any) {
|
||||
await ctx.trackerClient.deleteProjectFile(params.project_slug, params.file_id);
|
||||
return ok('File deleted.');
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { createStepTools } from './steps.js';
|
||||
import { createMessageTools } from './messages.js';
|
||||
import { createProjectTools } from './projects.js';
|
||||
import { createMemberTools } from './members.js';
|
||||
import { createFileTools } from './files.js';
|
||||
|
||||
/**
|
||||
* Create all Team Board tracker tools for the agent.
|
||||
@ -17,6 +18,7 @@ export function createTrackerTools(ctx: ToolContext): ToolDefinition[] {
|
||||
...createMessageTools(ctx),
|
||||
...createProjectTools(ctx),
|
||||
...createMemberTools(ctx),
|
||||
...createFileTools(ctx),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -111,15 +111,51 @@ export class TrackerClient {
|
||||
return this.request('GET', `/api/v1/messages?${qs}`);
|
||||
}
|
||||
|
||||
// --- Files (Attachments) ---
|
||||
// Files are attachments on Messages. To attach a file to a task,
|
||||
// send a message with task_id + attachment.
|
||||
// Upload endpoint: POST /api/v1/messages/{id}/attachments (TODO in Tracker)
|
||||
// Download endpoint: GET /api/v1/attachments/{id} (TODO in Tracker)
|
||||
// --- Project Files ---
|
||||
|
||||
// async uploadAttachment(messageId: string, filename: string, data: Buffer): Promise<Record<string, unknown>> {
|
||||
// return this.request('POST', `/api/v1/messages/${messageId}/attachments`, ...);
|
||||
// }
|
||||
async listProjectFiles(projectSlug: string, search?: string): Promise<Record<string, unknown>[]> {
|
||||
const qs = search ? `?search=${encodeURIComponent(search)}` : '';
|
||||
return this.request('GET', `/api/v1/projects/${projectSlug}/files${qs}`);
|
||||
}
|
||||
|
||||
async getProjectFile(projectSlug: string, fileId: string): Promise<Record<string, unknown>> {
|
||||
return this.request('GET', `/api/v1/projects/${projectSlug}/files/${fileId}`);
|
||||
}
|
||||
|
||||
async downloadProjectFile(projectSlug: string, fileId: string): Promise<string> {
|
||||
const url = `${this.baseUrl}/api/v1/projects/${projectSlug}/files/${fileId}/download`;
|
||||
const res = await fetch(url, {
|
||||
headers: { 'Authorization': `Bearer ${this.token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
async uploadProjectFile(projectSlug: string, filename: string, content: string, description?: string): Promise<Record<string, unknown>> {
|
||||
const url = `${this.baseUrl}/api/v1/projects/${projectSlug}/files`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob([Buffer.from(content, 'base64')]), filename);
|
||||
if (description) formData.append('description', description);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${this.token}` },
|
||||
body: formData,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`Upload failed: ${res.status} ${text}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async updateProjectFile(projectSlug: string, fileId: string, data: { description?: string }): Promise<Record<string, unknown>> {
|
||||
return this.request('PATCH', `/api/v1/projects/${projectSlug}/files/${fileId}`, data);
|
||||
}
|
||||
|
||||
async deleteProjectFile(projectSlug: string, fileId: string): Promise<void> {
|
||||
await this.request('DELETE', `/api/v1/projects/${projectSlug}/files/${fileId}`);
|
||||
}
|
||||
|
||||
// --- Projects ---
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user