Add upload tool and related APIs (#187)

* Add utils for r2 upload

* Add the tool and new routes

* fix auth issue

* sign headers

* add comment

* use our own API key to auth signed uploads

* Restructure things slightly

* tweak

* tweak

* add comments

* tweak

* revert a thing

* twaek

* drop mime type filtering

* new incarnation of mime type filtering

* jsut allow all octet-streams

* simplify further

* tweak

* update lockfile

---------

Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
This commit is contained in:
Mateusz Burzyński
2026-01-28 05:34:58 +00:00
committed by pullfrog[bot]
parent cac9b0e645
commit 071e885d63
27 changed files with 4777 additions and 690 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ function spawnBash(params: SpawnParams): ChildProcess {
// return useNamespaceIsolation
// ? spawn("unshare", ["--pid", "--fork", "--mount-proc", "bash", "-c", params.command], spawnOpts)
// : spawn("bash", ["-c", params.command], spawnOpts);
return spawn("bash", ["-c", params.command], spawnOpts);
return spawn("bash", ["-c", params.command], spawnOpts);
}
/** kill process and its entire process group */
+1 -1
View File
@@ -1,7 +1,7 @@
import { type } from "arktype";
import type { ToolContext } from "./server.ts";
import type { PrepResult } from "../prep/index.ts";
import { runPrepPhase } from "../prep/index.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
// empty schema for tools with no parameters
+3 -3
View File
@@ -6,7 +6,7 @@ import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export function CreateBranchTool(ctx: ToolContext) {
const defaultBranch = ctx.repo.repo.default_branch || "main";
const defaultBranch = ctx.repo.data.default_branch || "main";
const CreateBranch = type({
branchName: type.string.describe(
@@ -24,7 +24,7 @@ export function CreateBranchTool(ctx: ToolContext) {
parameters: CreateBranch,
execute: execute(async ({ branchName, baseBranch }) => {
// baseBranch should always be defined due to default, but TypeScript needs help
const resolvedBaseBranch = baseBranch || ctx.repo.repo.default_branch || "main";
const resolvedBaseBranch = baseBranch || ctx.repo.data.default_branch || "main";
// validate branch name for secrets
if (containsSecrets(branchName)) {
@@ -142,7 +142,7 @@ export const PushBranch = type({
});
export function PushBranchTool(ctx: ToolContext) {
const defaultBranch = ctx.repo.repo.default_branch || "main";
const defaultBranch = ctx.repo.data.default_branch || "main";
return tool({
name: "push_branch",
+5 -2
View File
@@ -8,7 +8,6 @@ import type { Mode } from "../modes.ts";
import type { PrepResult } from "../prep/index.ts";
import type { OctokitWithPlugins } from "../utils/github.ts";
import type { ResolvedPayload } from "../utils/payload.ts";
import type { RepoData } from "../utils/repoData.ts";
export type BackgroundProcess = {
pid: number;
@@ -53,10 +52,11 @@ export function initToolState(ctx: InitToolStateParams): ToolState {
}
export interface ToolContext {
repo: RepoData;
repo: RunContextData["repo"];
payload: ResolvedPayload;
octokit: OctokitWithPlugins;
githubInstallationToken: string;
apiToken: string;
agent: Agent;
modes: Mode[];
toolState: ToolState;
@@ -64,6 +64,7 @@ export interface ToolContext {
jobId: string | undefined;
}
import type { RunContextData } from "../utils/runContextData.ts";
import { BashTool, KillBackgroundTool } from "./bash.ts";
import { CheckoutPrTool } from "./checkout.ts";
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
@@ -90,6 +91,7 @@ import { CreatePullRequestReviewTool } from "./review.ts";
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
import { SelectModeTool } from "./selectMode.ts";
import { addTools } from "./shared.ts";
import { UploadFileTool } from "./upload.ts";
/**
* Find an available port starting from the given port
@@ -176,6 +178,7 @@ export async function startMcpHttpServer(
CreateBranchTool(ctx),
CommitFilesTool(ctx),
PushBranchTool(ctx),
UploadFileTool(ctx),
];
// only add BashTool when bash is "restricted"
+69
View File
@@ -0,0 +1,69 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { type } from "arktype";
import { fileTypeFromBuffer } from "file-type";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
const UploadFileParams = type({
path: type.string.describe("absolute path to file to upload"),
});
export function UploadFileTool(ctx: ToolContext) {
return tool({
name: "upload_file",
description:
"upload a file to get a permanent public URL. use for screenshots, artifacts, or any files you want to reference in PRs/comments. max 10MB, images/text/archives allowed.",
parameters: UploadFileParams,
execute: execute(async (params) => {
// read file from disk eagerly on purpose to avoid its content being changed by the time it's uploaded
const buffer = fs.readFileSync(params.path);
const filename = path.basename(params.path);
const contentLength = buffer.length;
const fileType = await fileTypeFromBuffer(buffer);
const contentType = fileType?.mime || "application/octet-stream";
const apiUrl = process.env.API_URL || "https://pullfrog.com";
const response = await fetch(`${apiUrl}/api/upload/signed-url`, {
method: "POST",
headers: {
Authorization: `Bearer ${ctx.apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
filename,
contentType,
contentLength,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`failed to get upload URL: ${error}`);
}
const { uploadUrl, publicUrl } = (await response.json()) as {
uploadUrl: string;
publicUrl: string;
};
const uploadResponse = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": contentType,
// should be set automatically, but given this header is signed it's better to be explicit
"Content-Length": String(contentLength),
},
body: buffer,
});
if (!uploadResponse.ok) {
throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
}
return { success: true, publicUrl, filename, contentLength, contentType };
}),
});
}