Skip to content

Commit 7b2f086

Browse files
Add delete file tool call (#47)
* Add file delete tool * Update instructions for local dev setup * Add tests * Prettier
1 parent 35f9413 commit 7b2f086

File tree

9 files changed

+5580
-4692
lines changed

9 files changed

+5580
-4692
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CLOUDFLARE_CLIENT_ID=
2+
CLOUDFLARE_CLIENT_SECRET=

apps/sandbox-container/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@ This is a simple MCP-based interface for a sandboxed development environment.
44

55
## Local dev
66

7-
Currently a work in progress. Cloudchamber local dev isn't implemented yet, so we are doing a bit of a hack to just run the server in your local environment.
7+
Cloudchamber local dev isn't implemented yet, so we are doing a bit of a hack to just run the server in your local environment. Because of this, testing the container(s) and container manager locally is not possible at this time.
88

9-
TODO: replace locally running server with the real docker container.
9+
Do the following from within the sandbox-container app:
10+
11+
1. Copy the `.dev.vars.example` file to a new `.dev.vars` file.
12+
2. Get the Cloudflare client id and secret from a team member and add them to the `.dev.vars` file.
13+
3. Run `pnpm i` then `pnpm dev` to start the MCP server.
14+
4. Run `pnpx @modelcontextprotocol/inspector` to start the MCP inspector client.
15+
5. Open the inspector client in your browser and connect to the server via `http://localhost:8976/workers/sandbox/sse`.
16+
17+
Note: Temporary files created through files tool calls are stored in the workdir folder of this app.
1018

1119
## Deploying
1220

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { get_file_name_from_path } from './fileUtils'
4+
5+
describe('get_file_name_from_path', () => {
6+
it('strips files/contents', async () => {
7+
const path = await get_file_name_from_path('/files/contents/cats')
8+
expect(path).toBe('/cats')
9+
}),
10+
it('works if files/contents is not present', async () => {
11+
const path = await get_file_name_from_path('/dogs')
12+
expect(path).toBe('/dogs')
13+
}),
14+
it('strips a trailing slash', async () => {
15+
const path = await get_file_name_from_path('/files/contents/birds/')
16+
expect(path).toBe('/birds')
17+
})
18+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export async function get_file_name_from_path(path: string): Promise<string> {
2+
path = path.replace('/files/contents', '')
3+
path = path.endsWith('/') ? path.substring(0, path.length - 1) : path
4+
5+
return path
6+
}

apps/sandbox-container/container/index.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { Hono } from 'hono'
77
import { streamText } from 'hono/streaming'
88
import mime from 'mime'
99

10-
import { ExecParams, FileList, FilesWrite } from '../shared/schema.ts'
10+
import { ExecParams, FilesWrite } from '../shared/schema.ts'
11+
import { get_file_name_from_path } from './fileUtils.ts'
12+
13+
import type { FileList } from '../shared/schema.ts'
1114

1215
process.chdir('workdir')
1316

@@ -60,8 +63,7 @@ app.get('/files/ls', async (c) => {
6063
* Get the contents of a file or directory
6164
*/
6265
app.get('/files/contents/*', async (c) => {
63-
let reqPath = c.req.path.replace('/files/contents', '')
64-
reqPath = reqPath.endsWith('/') ? reqPath.substring(0, reqPath.length - 1) : reqPath
66+
const reqPath = await get_file_name_from_path(c.req.path)
6567
try {
6668
const mimeType = mime.getType(reqPath)
6769
const headers = mimeType ? { 'Content-Type': mimeType } : undefined
@@ -105,7 +107,8 @@ app.get('/files/contents/*', async (c) => {
105107
*/
106108
app.post('/files/contents', zValidator('json', FilesWrite), async (c) => {
107109
const file = c.req.valid('json')
108-
const reqPath = file.path.endsWith('/') ? file.path.substring(0, file.path.length - 1) : file.path
110+
const reqPath = await get_file_name_from_path(file.path)
111+
109112
try {
110113
await fs.writeFile(reqPath, file.text)
111114
return c.newResponse(null, 200)
@@ -114,6 +117,28 @@ app.post('/files/contents', zValidator('json', FilesWrite), async (c) => {
114117
}
115118
})
116119

120+
/**
121+
* DELETE /files/contents/{filepath}
122+
*
123+
* Delete a file or directory
124+
*/
125+
app.delete('/files/contents/*', async (c) => {
126+
const reqPath = await get_file_name_from_path(c.req.path)
127+
128+
try {
129+
await fs.rm(path.join(process.cwd(), reqPath), { recursive: true })
130+
return c.newResponse('ok', 200)
131+
} catch (e: any) {
132+
if (e.code) {
133+
if (e.code === 'ENOENT') {
134+
return c.notFound()
135+
}
136+
}
137+
138+
throw e
139+
}
140+
})
141+
117142
/**
118143
* POST /exec
119144
*

apps/sandbox-container/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"start": "wrangler dev",
1212
"start:container": "tsx container/index.ts",
1313
"postinstall": "mkdir -p workdir",
14+
"test": "vitest",
1415
"types": "wrangler types"
1516
},
1617
"dependencies": {

apps/sandbox-container/server/containerMcp.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { McpAgent } from 'agents/mcp'
33
import { z } from 'zod'
44

55
import { OPEN_CONTAINER_PORT } from '../shared/consts'
6-
import { ExecParams, FileList, FilesWrite } from '../shared/schema'
6+
import { ExecParams, FilePathParam, FilesWrite } from '../shared/schema'
77
import { MAX_CONTAINERS, proxyFetch, startAndWaitForPort } from './containerHelpers'
88
import { getContainerManager } from './containerManager'
99
import { BASE_INSTRUCTIONS } from './prompts'
1010
import { fileToBase64 } from './utils'
11-
import { Env, Props } from '.'
11+
12+
import type { FileList } from '../shared/schema'
13+
import type { Env, Props } from '.'
1214

1315
export class ContainerMcpAgent extends McpAgent<Env, Props> {
1416
server = new McpServer(
@@ -70,6 +72,17 @@ export class ContainerMcpAgent extends McpAgent<Env, Props> {
7072
}
7173
}
7274
)
75+
this.server.tool(
76+
'container_file_delete',
77+
'Delete file and its contents',
78+
{ args: FilePathParam },
79+
async ({ args }) => {
80+
const deleted = await this.container_file_delete(args)
81+
return {
82+
content: [{ type: 'text', text: `File deleted: ${deleted}.` }],
83+
}
84+
}
85+
)
7386
this.server.tool(
7487
'container_files_write',
7588
'Write file contents',
@@ -229,11 +242,20 @@ export class ContainerMcpAgent extends McpAgent<Env, Props> {
229242
return json
230243
}
231244

245+
async container_file_delete(filePath: string): Promise<boolean> {
246+
const res = await proxyFetch(
247+
this.env.ENVIRONMENT,
248+
this.ctx.container,
249+
new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents/${filePath}`, {
250+
method: 'DELETE',
251+
}),
252+
OPEN_CONTAINER_PORT
253+
)
254+
return res.ok
255+
}
232256
async container_files_read(
233257
filePath: string
234258
): Promise<{ blob: Blob; mimeType: string | undefined }> {
235-
console.log('reading')
236-
console.log(filePath)
237259
const res = await proxyFetch(
238260
this.env.ENVIRONMENT,
239261
this.ctx.container,
@@ -269,7 +291,6 @@ export class ContainerMcpAgent extends McpAgent<Env, Props> {
269291
if (!res || !res.ok) {
270292
throw new Error(`Request to container failed: ${await res.text()}`)
271293
}
272-
const txt = await res.text()
273294
return `Wrote file: ${file.path}`
274295
}
275296
}

apps/sandbox-container/shared/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export const FilesWrite = z.object({
1313
text: z.string().describe('Full text content of the file you want to write.'),
1414
})
1515

16+
export type FilePathParam = z.infer<typeof FilePathParam>
17+
export const FilePathParam = z.string()
18+
1619
export type FileList = z.infer<typeof FileList>
1720
export const FileList = z.object({
1821
resources: z

0 commit comments

Comments
 (0)