Skip to content

Commit 6cc5ddc

Browse files
authored
Merge pull request #39 from cloudflare/csparks/add-container-mcp
Add container MCP server
2 parents 8b60055 + 3e1f73e commit 6cc5ddc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2611
-734
lines changed

.eslintrc.cjs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// This configuration only applies to the package manager root.
22
/** @type {import("eslint").Linter.Config} */
33
module.exports = {
4-
ignorePatterns: [
5-
'apps/**',
6-
'packages/**',
7-
],
8-
extends: ['@repo/eslint-config/default.cjs']
4+
ignorePatterns: ['apps/**', 'packages/**'],
5+
extends: ['@repo/eslint-config/default.cjs'],
96
}

.vscode/extensions.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
44

55
// List of extensions which should be recommended for users of this workspace.
6-
"recommendations": [
7-
"esbenp.prettier-vscode",
8-
"dbaeumer.vscode-eslint",
9-
],
6+
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"],
107
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
118
"unwantedRecommendations": []
129
}

.vscode/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
"**/packages/tools/bin/*": "shellscript",
1414
"**/*.css": "tailwindcss",
1515
"turbo.json": "jsonc",
16-
"**/packages/typescript-config/*.json": "jsonc",
16+
"**/packages/typescript-config/*.json": "jsonc"
1717
},
1818
"eslint.workingDirectories": [
1919
{
2020
"mode": "auto"
2121
}
22-
],
22+
]
2323
}

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ Model Context Protocol (MCP) is a [new, standardized protocol](https://modelcont
44

55
This lets you use Claude Desktop, or any MCP Client, to use natural language to accomplish things on your Cloudflare account, e.g.:
66

7-
* `List all the Cloudflare workers on my <some-email>@gmail.com account.`
8-
* `Can you tell me about any potential issues on this particular worker '...'?`
7+
- `List all the Cloudflare workers on my <some-email>@gmail.com account.`
8+
- `Can you tell me about any potential issues on this particular worker '...'?`
99

1010
## Access the remote MCP server from Claude Desktop
1111

1212
Open Claude Desktop and navigate to Settings -> Developer -> Edit Config. This opens the configuration file that controls which MCP servers Claude can access.
1313

14-
Replace the content with the following configuration. Once you restart Claude Desktop, a browser window will open showing your OAuth login page. Complete the authentication flow to grant Claude access to your MCP server. After you grant access, the tools will become available for you to use.
14+
Replace the content with the following configuration. Once you restart Claude Desktop, a browser window will open showing your OAuth login page. Complete the authentication flow to grant Claude access to your MCP server. After you grant access, the tools will become available for you to use.
1515

1616
```
1717
{
@@ -44,17 +44,20 @@ Ensure your Cloudflare account has the necessary subscription level for the feat
4444
## Features
4545

4646
### Workers Management
47+
4748
- `worker_list`: List all Workers in your account
4849
- `worker_get_worker`: Get a Worker's script content
4950

5051
### Workers Logs
52+
5153
- `worker_logs_by_worker_name`: Analyze recent logs for a Cloudflare Worker by worker name
5254
- `worker_logs_by_ray_id`: Analyze recent logs across all workers for a specific request by Cloudflare Ray ID
5355
- `worker_logs_keys`: Get available telemetry keys for a Cloudflare Worker
5456

5557
## Developing
5658

5759
### Apps
60+
5861
- [workers-observability](apps/workers-observability/): The Workers Observability MCP server
5962

6063
### Packages

apps/sandbox-container/Dockerfile

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Use an official Ubuntu as a base image
2+
FROM ubuntu:20.04
3+
4+
# Set non-interactive mode to avoid prompts during package installation
5+
ARG DEBIAN_FRONTEND=noninteractive
6+
7+
# Use bash for the shell
8+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
9+
10+
# Update and install useful CLI utilities
11+
RUN apt-get update && apt-get install -y \
12+
curl \
13+
git \
14+
htop \
15+
vim \
16+
wget \
17+
net-tools \
18+
build-essential \
19+
nmap \
20+
sudo \
21+
ca-certificates \
22+
lsb-release \
23+
nodejs \
24+
npm \
25+
python3 \
26+
python3-pip \
27+
&& apt-get clean
28+
29+
RUN pip3 install matplotlib numpy pandas
30+
31+
# Install Node.js and Corepack
32+
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
33+
&& apt-get install -y nodejs \
34+
&& corepack enable
35+
36+
# ENV NODE_VERSION 22.14.0
37+
38+
# Download and install nvm
39+
# RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | PROFILE="${BASH_ENV}" bash
40+
# RUN echo node > .nvmrc
41+
# RUN nvm install $NODE_VERSION
42+
# RUN nvm use $NODE_VERSION
43+
44+
# Set working directory
45+
WORKDIR /app
46+
47+
# Expose the port your Node.js server will run on
48+
EXPOSE 8080
49+
50+
COPY . ./
51+
52+
RUN npm i
53+
54+
# add node and npm to path so the commands are available
55+
# ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
56+
# ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
57+
58+
# Default command (run your Node.js server here)
59+
CMD ["npm", "run", "start:container"]

apps/sandbox-container/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Container MCP Server
2+
3+
This is a simple MCP-based interface for a sandboxed development environment.
4+
5+
## Local dev
6+
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.
8+
9+
TODO: replace locally running server with the real docker container.
10+
11+
## Deploying
12+
13+
1. Make sure the docker daemon is running
14+
15+
2. Disable WARP and run
16+
17+
```
18+
npx https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/14387504770/npm-package-wrangler-8740 deploy
19+
```
20+
21+
3. Add to your Claude config. If using with Claude, you'll need to disable WARP:
22+
23+
```
24+
{
25+
"mcpServers": {
26+
"container": {
27+
"command": "npx",
28+
"args": [
29+
"mcp-remote",
30+
// this is my deployed instance
31+
"https://container-starter-2.cmsparks.workers.dev/sse"
32+
]
33+
}
34+
}
35+
}
36+
```
37+
38+
## Tools
39+
40+
- `container_initialize`: (Re)start a container. Containers are intended to be ephemeral and don't save any state. Containers are only guaranteed to last 10m (this is just because I have a max of like ~5 containers per account).
41+
- `container_ping`: Ping a container for connectivity
42+
- `container_exec`: Run a command in the shell
43+
- `container_files_write`: Write to a file
44+
- `container_files_list`: List all files in the work directory
45+
- `container_file_read`: Read the contents of a single file or directory
46+
47+
## Resources
48+
49+
TODO
50+
51+
Tried implementing these, but MCP clients don't support resources well at all.
52+
53+
## Prompts
54+
55+
TODO
56+
57+
## Container support
58+
59+
The container currently runs python and node. It's connected to the internet and LLMs can install whatever packages.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { exec } from 'node:child_process'
2+
import * as fs from 'node:fs/promises'
3+
import path from 'node:path'
4+
import { serve } from '@hono/node-server'
5+
import { zValidator } from '@hono/zod-validator'
6+
import { Hono } from 'hono'
7+
import { streamText } from 'hono/streaming'
8+
import mime from 'mime'
9+
10+
import { ExecParams, FileList, FilesWrite } from '../shared/schema.ts'
11+
12+
process.chdir('workdir')
13+
14+
const app = new Hono()
15+
16+
app.get('/ping', (c) => c.text('pong!'))
17+
18+
/**
19+
* GET /files/ls
20+
*
21+
* Gets all files in a directory
22+
*/
23+
app.get('/files/ls', async (c) => {
24+
const directoriesToRead = ['.']
25+
const files: FileList = { resources: [] }
26+
27+
while (directoriesToRead.length > 0) {
28+
const curr = directoriesToRead.pop()
29+
if (!curr) {
30+
throw new Error('Popped empty stack, error while listing directories')
31+
}
32+
const fullPath = path.join(process.cwd(), curr)
33+
const dir = await fs.readdir(fullPath, { withFileTypes: true })
34+
for (const dirent of dir) {
35+
const relPath = path.relative(process.cwd(), `${fullPath}/${dirent.name}`)
36+
if (dirent.isDirectory()) {
37+
directoriesToRead.push(dirent.name)
38+
files.resources.push({
39+
uri: `file:///${relPath}`,
40+
name: dirent.name,
41+
mimeType: 'inode/directory',
42+
})
43+
} else {
44+
const mimeType = mime.getType(dirent.name)
45+
files.resources.push({
46+
uri: `file:///${relPath}`,
47+
name: dirent.name,
48+
mimeType: mimeType ?? undefined,
49+
})
50+
}
51+
}
52+
}
53+
54+
return c.json(files)
55+
})
56+
57+
/**
58+
* GET /files/contents/{filepath}
59+
*
60+
* Get the contents of a file or directory
61+
*/
62+
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
65+
try {
66+
const mimeType = mime.getType(reqPath)
67+
const headers = mimeType ? { 'Content-Type': mimeType } : undefined
68+
const contents = await fs.readFile(path.join(process.cwd(), reqPath))
69+
return c.newResponse(contents, 200, headers)
70+
} catch (e: any) {
71+
if (e.code) {
72+
// handle directory
73+
if (e.code === 'EISDIR') {
74+
const files: string[] = []
75+
const dir = await fs.readdir(path.join(process.cwd(), reqPath), {
76+
withFileTypes: true,
77+
})
78+
for (const dirent of dir) {
79+
const relPath = path.relative(process.cwd(), `${reqPath}/${dirent.name}`)
80+
if (dirent.isDirectory()) {
81+
files.push(`file:///${relPath}`)
82+
} else {
83+
const mimeType = mime.getType(dirent.name)
84+
files.push(`file:///${relPath}`)
85+
}
86+
}
87+
return c.newResponse(files.join('\n'), 200, {
88+
'Content-Type': 'inode/directory',
89+
})
90+
}
91+
92+
if (e.code === 'ENOENT') {
93+
return c.notFound()
94+
}
95+
}
96+
97+
throw e
98+
}
99+
})
100+
101+
/**
102+
* POST /files/contents
103+
*
104+
* Create or update file contents
105+
*/
106+
app.post('/files/contents', zValidator('json', FilesWrite), async (c) => {
107+
const file = c.req.valid('json')
108+
const reqPath = file.path.endsWith('/') ? file.path.substring(0, file.path.length - 1) : file.path
109+
try {
110+
await fs.writeFile(reqPath, file.text)
111+
return c.newResponse(null, 200)
112+
} catch (e) {
113+
return c.newResponse(`Error: ${e}`, 400)
114+
}
115+
})
116+
117+
/**
118+
* POST /exec
119+
*
120+
* Execute a command in a shell
121+
*/
122+
app.post('/exec', zValidator('json', ExecParams), (c) => {
123+
const execParams = c.req.valid('json')
124+
const proc = exec(execParams.args)
125+
return streamText(c, async (stream) => {
126+
return new Promise(async (resolve, reject) => {
127+
if (proc.stdout) {
128+
// Stream data from stdout
129+
proc.stdout.on('data', async (data) => {
130+
await stream.write(data.toString())
131+
})
132+
} else {
133+
await stream.write('WARNING: no stdout stream for process')
134+
}
135+
136+
if (execParams.streamStderr) {
137+
if (proc.stderr) {
138+
proc.stderr.on('data', async (data) => {
139+
await stream.write(data.toString())
140+
})
141+
} else {
142+
await stream.write('WARNING: no stderr stream for process')
143+
}
144+
}
145+
146+
// Handle process exit
147+
proc.on('exit', async (code) => {
148+
await stream.write(`Process exited with code: ${code}`)
149+
if (code === 0) {
150+
stream.close()
151+
resolve()
152+
} else {
153+
console.error(`Process exited with code ${code}`)
154+
reject(new Error(`Process failed with code ${code}`))
155+
}
156+
})
157+
158+
proc.on('error', (err) => {
159+
console.error('Error with process: ', err)
160+
reject(err)
161+
})
162+
})
163+
})
164+
})
165+
166+
serve({
167+
fetch: app.fetch,
168+
port: 8080,
169+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["es2023"],
4+
"module": "ES2022",
5+
"target": "es2022",
6+
"types": ["@types/node"],
7+
"strict": true,
8+
"esModuleInterop": true,
9+
"skipLibCheck": true,
10+
"moduleResolution": "bundler",
11+
"noEmit": true,
12+
"allowImportingTsExtensions": true
13+
},
14+
"include": ["**/*.ts"],
15+
"exclude": []
16+
}

0 commit comments

Comments
 (0)