@@ -2,16 +2,12 @@ import { McpAgent } from 'agents/mcp'
22
33import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
44
5- import { OPEN_CONTAINER_PORT } from '../shared/consts'
65import { ExecParams , FilePathParam , FileWrite } from '../shared/schema'
7- import { MAX_CONTAINERS , proxyFetch , startAndWaitForPort } from './containerHelpers'
8- import { getContainerManager } from './containerManager'
96import { BASE_INSTRUCTIONS } from './prompts'
107import { fileToBase64 , stripProtocolFromFilePath } from './utils'
118
12- import type { FileList } from '../shared/schema'
139import type { Env } from './context'
14- import type { Props } from '.'
10+ import type { Props , UserContainer } from '.'
1511
1612export class ContainerMcpAgent extends McpAgent < Env , { } , Props > {
1713 _server : CloudflareMCPServer | undefined
@@ -27,6 +23,11 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
2723 return this . _server
2824 }
2925
26+ get userContainer ( ) : DurableObjectStub < UserContainer > {
27+ const userContainer = this . env . USER_CONTAINER . idFromName ( this . props . user . id )
28+ return this . env . USER_CONTAINER . get ( userContainer )
29+ }
30+
3031 constructor (
3132 public ctx : DurableObjectState ,
3233 public env : Env
@@ -35,21 +36,6 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
3536 super ( ctx , env )
3637 }
3738
38- async destroyContainer ( ) : Promise < void > {
39- await this . ctx . container ?. destroy ( )
40- }
41-
42- async killContainer ( ) : Promise < void > {
43- console . log ( 'Reaping container' )
44- const containerManager = getContainerManager ( this . env )
45- const active = await containerManager . listActive ( )
46- if ( this . ctx . id . toString ( ) in active ) {
47- console . log ( 'killing container' )
48- await this . destroyContainer ( )
49- await containerManager . killContainer ( this . ctx . id . toString ( ) )
50- }
51- }
52-
5339 async init ( ) {
5440 this . props . user . id
5541 this . server = new CloudflareMCPServer ( {
@@ -64,78 +50,63 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
6450
6551 this . server . tool (
6652 'container_initialize' ,
67- ' Start or reset the container' ,
68- { } ,
53+ ` Start or restart the container.
54+ Use this tool to initialize a container before running any python or node.js code that the user requests ro run.` ,
6955 // @ts -ignore
70- async ( { } ) => {
56+ async ( ) => {
57+ const userInBlocklist = await this . env . USER_BLOCKLIST . get ( this . props . user . id )
58+ if ( userInBlocklist ) {
59+ return {
60+ content : [ { type : 'text' , text : "Blocked from intializing container." } ] ,
61+ }
62+ }
7163 return {
72- content : [ { type : 'text' , text : await this . container_initialize ( ) } ] ,
64+ content : [ { type : 'text' , text : await this . userContainer . container_initialize ( ) } ] ,
7365 }
7466 }
7567 )
7668
77- this . server . tool ( 'container_ping' , ' Ping the container for liveliness' , { } , async ( { } ) => {
69+ this . server . tool ( 'container_ping' , ` Ping the container for liveliness. Use this tool to check if the container is running.` , { } , async ( { } ) => {
7870 return {
79- content : [ { type : 'text' , text : await this . container_ping ( ) } ] ,
71+ content : [ { type : 'text' , text : await this . userContainer . container_ping ( ) } ] ,
8072 }
8173 } )
8274 this . server . tool (
8375 'container_exec' ,
84- 'Run a command in a container and return the results from stdout' ,
76+ 'Run a command in a container and return the results from stdout. If necessary, set a timeout. To debug, stream back standard error. ' ,
8577 { args : ExecParams } ,
8678 async ( { args } ) => {
8779 return {
88- content : [ { type : 'text' , text : await this . container_exec ( args ) } ] ,
80+ content : [ { type : 'text' , text : await this . userContainer . container_exec ( args ) } ] ,
8981 }
9082 }
9183 )
9284 this . server . tool (
9385 'container_file_delete' ,
94- 'Delete file and its contents ' ,
86+ 'Delete file in the working directory ' ,
9587 { args : FilePathParam } ,
9688 async ( { args } ) => {
9789 const path = await stripProtocolFromFilePath ( args . path )
98- const deleted = await this . container_file_delete ( path )
90+ const deleted = await this . userContainer . container_file_delete ( path )
9991 return {
10092 content : [ { type : 'text' , text : `File deleted: ${ deleted } .` } ] ,
10193 }
10294 }
10395 )
10496 this . server . tool (
10597 'container_file_write' ,
106- 'Create a new file with the provided contents, overwriting the file if it already exists' ,
98+ 'Create a new file with the provided contents in the working direcotry , overwriting the file if it already exists' ,
10799 { args : FileWrite } ,
108100 async ( { args } ) => {
109101 args . path = await stripProtocolFromFilePath ( args . path )
110102 return {
111- content : [ { type : 'text' , text : await this . container_file_write ( args ) } ] ,
103+ content : [ { type : 'text' , text : await this . userContainer . container_file_write ( args ) } ] ,
112104 }
113105 }
114106 )
115- this . server . tool ( 'container_files_list' , 'List working directory file tree' , { } , async ( { } ) => {
116- // This approach relies on resources, which aren't handled well by Claude right now. Until that's sorted, we can just use file read, since it lists all files in a directory if a directory is passed to it.
117- //const files = await this.container_ls()
118-
119- // const resources: {
120- // type: 'resource'
121- // resource: { uri: string; text: string; mimeType: string }
122- // }[] = files.resources.map((r) => {
123- // return {
124- // type: 'resource',
125- // resource: {
126- // uri: r.uri,
127- // text: r.uri,
128- // mimeType: 'text/plain',
129- // },
130- // }
131- // })
132-
133- // return {
134- // content: resources,
135- // }
136-
107+ this . server . tool ( 'container_files_list' , 'List working directory file tree. This just reads the contents of the current working directory' , { } , async ( { } ) => {
137108 // Begin workaround using container read rather than ls:
138- const { blob, mimeType } = await this . container_file_read ( '.' )
109+ const { blob, mimeType } = await this . userContainer . container_file_read ( '.' )
139110 return {
140111 content : [
141112 {
@@ -155,7 +126,7 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
155126 { args : FilePathParam } ,
156127 async ( { args } ) => {
157128 const path = await stripProtocolFromFilePath ( args . path )
158- const { blob, mimeType } = await this . container_file_read ( path )
129+ const { blob, mimeType } = await this . userContainer . container_file_read ( path )
159130
160131 if ( mimeType && mimeType . startsWith ( 'text' ) ) {
161132 return {
@@ -176,6 +147,8 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
176147 {
177148 type : 'resource' ,
178149 resource : {
150+ // for some reason the RPC type for Blob is not exactly the same as the regular Blob type
151+ // @ts -ignore
179152 blob : await fileToBase64 ( blob ) ,
180153 uri : `file://${ path } ` ,
181154 mimeType : mimeType ,
@@ -187,134 +160,4 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
187160 }
188161 )
189162 }
190-
191- async container_initialize ( ) : Promise < string > {
192- // kill container
193- await this . killContainer ( )
194-
195- // try to cleanup cleanup old containers
196- const containerManager = getContainerManager ( this . env )
197-
198- if ( ( await containerManager . listActive ( ) ) . length >= MAX_CONTAINERS ) {
199- await containerManager . tryKillOldContainers ( )
200- if ( ( await containerManager . listActive ( ) ) . length >= MAX_CONTAINERS ) {
201- throw new Error (
202- `Unable to reap enough containers. There are ${ MAX_CONTAINERS } active container sandboxes, please wait`
203- )
204- }
205- }
206-
207- // start container
208- let startedContainer = false
209- await this . ctx . blockConcurrencyWhile ( async ( ) => {
210- startedContainer = await startAndWaitForPort (
211- this . env . ENVIRONMENT ,
212- this . ctx . container ,
213- OPEN_CONTAINER_PORT
214- )
215- } )
216- if ( ! startedContainer ) {
217- throw new Error ( 'Failed to start container' )
218- }
219-
220- // track and manage lifecycle
221- containerManager . trackContainer ( this . ctx . id . toString ( ) )
222-
223- return `Created new container`
224- }
225-
226- async container_ping ( ) : Promise < string > {
227- const res = await proxyFetch (
228- this . env . ENVIRONMENT ,
229- this . ctx . container ,
230- new Request ( `http://host:${ OPEN_CONTAINER_PORT } /ping` ) ,
231- OPEN_CONTAINER_PORT
232- )
233- if ( ! res || ! res . ok ) {
234- throw new Error ( `Request to container failed: ${ await res . text ( ) } ` )
235- }
236- return await res . text ( )
237- }
238-
239- async container_exec ( params : ExecParams ) : Promise < string > {
240- const res = await proxyFetch (
241- this . env . ENVIRONMENT ,
242- this . ctx . container ,
243- new Request ( `http://host:${ OPEN_CONTAINER_PORT } /exec` , {
244- method : 'POST' ,
245- body : JSON . stringify ( params ) ,
246- headers : {
247- 'content-type' : 'application/json' ,
248- } ,
249- } ) ,
250- OPEN_CONTAINER_PORT
251- )
252- if ( ! res || ! res . ok ) {
253- throw new Error ( `Request to container failed: ${ await res . text ( ) } ` )
254- }
255- const txt = await res . text ( )
256- return txt
257- }
258-
259- async container_ls ( ) : Promise < FileList > {
260- const res = await proxyFetch (
261- this . env . ENVIRONMENT ,
262- this . ctx . container ,
263- new Request ( `http://host:${ OPEN_CONTAINER_PORT } /files/ls` ) ,
264- OPEN_CONTAINER_PORT
265- )
266- if ( ! res || ! res . ok ) {
267- throw new Error ( `Request to container failed: ${ await res . text ( ) } ` )
268- }
269- const json = ( await res . json ( ) ) as FileList
270- return json
271- }
272-
273- async container_file_delete ( filePath : string ) : Promise < boolean > {
274- const res = await proxyFetch (
275- this . env . ENVIRONMENT ,
276- this . ctx . container ,
277- new Request ( `http://host:${ OPEN_CONTAINER_PORT } /files/contents/${ filePath } ` , {
278- method : 'DELETE' ,
279- } ) ,
280- OPEN_CONTAINER_PORT
281- )
282- return res . ok
283- }
284- async container_file_read (
285- filePath : string
286- ) : Promise < { blob : Blob ; mimeType : string | undefined } > {
287- const res = await proxyFetch (
288- this . env . ENVIRONMENT ,
289- this . ctx . container ,
290- new Request ( `http://host:${ OPEN_CONTAINER_PORT } /files/contents/${ filePath } ` ) ,
291- OPEN_CONTAINER_PORT
292- )
293- if ( ! res || ! res . ok ) {
294- throw new Error ( `Request to container failed: ${ await res . text ( ) } ` )
295- }
296- return {
297- blob : await res . blob ( ) ,
298- mimeType : res . headers . get ( 'Content-Type' ) ?? undefined ,
299- }
300- }
301-
302- async container_file_write ( file : FileWrite ) : Promise < string > {
303- const res = await proxyFetch (
304- this . env . ENVIRONMENT ,
305- this . ctx . container ,
306- new Request ( `http://host:${ OPEN_CONTAINER_PORT } /files/contents` , {
307- method : 'POST' ,
308- body : JSON . stringify ( file ) ,
309- headers : {
310- 'content-type' : 'application/json' ,
311- } ,
312- } ) ,
313- OPEN_CONTAINER_PORT
314- )
315- if ( ! res || ! res . ok ) {
316- throw new Error ( `Request to container failed: ${ await res . text ( ) } ` )
317- }
318- return `Wrote file: ${ file . path } `
319- }
320163}
0 commit comments