@@ -2,16 +2,12 @@ import { McpAgent } from 'agents/mcp'
2
2
3
3
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
4
4
5
- import { OPEN_CONTAINER_PORT } from '../shared/consts'
6
5
import { ExecParams , FilePathParam , FileWrite } from '../shared/schema'
7
- import { MAX_CONTAINERS , proxyFetch , startAndWaitForPort } from './containerHelpers'
8
- import { getContainerManager } from './containerManager'
9
6
import { BASE_INSTRUCTIONS } from './prompts'
10
7
import { fileToBase64 , stripProtocolFromFilePath } from './utils'
11
8
12
- import type { FileList } from '../shared/schema'
13
9
import type { Env } from './context'
14
- import type { Props } from '.'
10
+ import type { Props , UserContainer } from '.'
15
11
16
12
export class ContainerMcpAgent extends McpAgent < Env , { } , Props > {
17
13
_server : CloudflareMCPServer | undefined
@@ -27,6 +23,11 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
27
23
return this . _server
28
24
}
29
25
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
+
30
31
constructor (
31
32
public ctx : DurableObjectState ,
32
33
public env : Env
@@ -35,21 +36,6 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
35
36
super ( ctx , env )
36
37
}
37
38
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
-
53
39
async init ( ) {
54
40
this . props . user . id
55
41
this . server = new CloudflareMCPServer ( {
@@ -64,78 +50,63 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
64
50
65
51
this . server . tool (
66
52
'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.` ,
69
55
// @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
+ }
71
63
return {
72
- content : [ { type : 'text' , text : await this . container_initialize ( ) } ] ,
64
+ content : [ { type : 'text' , text : await this . userContainer . container_initialize ( ) } ] ,
73
65
}
74
66
}
75
67
)
76
68
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 ( { } ) => {
78
70
return {
79
- content : [ { type : 'text' , text : await this . container_ping ( ) } ] ,
71
+ content : [ { type : 'text' , text : await this . userContainer . container_ping ( ) } ] ,
80
72
}
81
73
} )
82
74
this . server . tool (
83
75
'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. ' ,
85
77
{ args : ExecParams } ,
86
78
async ( { args } ) => {
87
79
return {
88
- content : [ { type : 'text' , text : await this . container_exec ( args ) } ] ,
80
+ content : [ { type : 'text' , text : await this . userContainer . container_exec ( args ) } ] ,
89
81
}
90
82
}
91
83
)
92
84
this . server . tool (
93
85
'container_file_delete' ,
94
- 'Delete file and its contents ' ,
86
+ 'Delete file in the working directory ' ,
95
87
{ args : FilePathParam } ,
96
88
async ( { args } ) => {
97
89
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 )
99
91
return {
100
92
content : [ { type : 'text' , text : `File deleted: ${ deleted } .` } ] ,
101
93
}
102
94
}
103
95
)
104
96
this . server . tool (
105
97
'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' ,
107
99
{ args : FileWrite } ,
108
100
async ( { args } ) => {
109
101
args . path = await stripProtocolFromFilePath ( args . path )
110
102
return {
111
- content : [ { type : 'text' , text : await this . container_file_write ( args ) } ] ,
103
+ content : [ { type : 'text' , text : await this . userContainer . container_file_write ( args ) } ] ,
112
104
}
113
105
}
114
106
)
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 ( { } ) => {
137
108
// 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 ( '.' )
139
110
return {
140
111
content : [
141
112
{
@@ -155,7 +126,7 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
155
126
{ args : FilePathParam } ,
156
127
async ( { args } ) => {
157
128
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 )
159
130
160
131
if ( mimeType && mimeType . startsWith ( 'text' ) ) {
161
132
return {
@@ -176,6 +147,8 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
176
147
{
177
148
type : 'resource' ,
178
149
resource : {
150
+ // for some reason the RPC type for Blob is not exactly the same as the regular Blob type
151
+ // @ts -ignore
179
152
blob : await fileToBase64 ( blob ) ,
180
153
uri : `file://${ path } ` ,
181
154
mimeType : mimeType ,
@@ -187,134 +160,4 @@ export class ContainerMcpAgent extends McpAgent<Env, {}, Props> {
187
160
}
188
161
)
189
162
}
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
- }
320
163
}
0 commit comments