Skip to content

Commit 26777a0

Browse files
author
BrokenDuck
committed
Add upload tests
1 parent 74bef11 commit 26777a0

File tree

5 files changed

+366
-191
lines changed

5 files changed

+366
-191
lines changed

mcp-run-python/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"imports": {
1616
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.17.4",
1717
"@std/cli": "jsr:@std/cli@^1.0.15",
18+
"@std/encoding": "jsr:@std/encoding@^1.0.10",
1819
"@std/fs": "jsr:@std/fs@^1.0.19",
1920
"@std/media-types": "jsr:@std/media-types@^1.1.0",
2021
"@std/path": "jsr:@std/path@^1.0.8",

mcp-run-python/deno.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mcp-run-python/src/files.ts

Lines changed: 15 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { exists } from '@std/fs/exists'
33
import { contentType } from '@std/media-types'
44
import { type McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
55
import z from 'zod'
6+
import { encodeBase64 } from '@std/encoding/base64'
67

78
/**
89
* Returns the temporary directory in the local filesystem for file persistence.
@@ -11,56 +12,6 @@ export function createRootDir(): string {
1112
return Deno.makeTempDirSync({ prefix: 'mcp_run_python' })
1213
}
1314

14-
/**
15-
* Streams a file from an http(s) or file:// URI into targetPath.
16-
* Returns when bytes are fully written.
17-
*/
18-
async function uploadFromUri(
19-
uri: string,
20-
absPath: string,
21-
): Promise<void> {
22-
await Deno.mkdir(
23-
path.dirname(absPath),
24-
{ recursive: true },
25-
)
26-
27-
if (uri.startsWith('http://') || uri.startsWith('https://')) {
28-
const res = await fetch(uri)
29-
if (!res.ok || !res.body) {
30-
throw new Error(`Fetch failed: ${res.status} ${res.statusText}`)
31-
}
32-
const file = await Deno.open(absPath, {
33-
write: true,
34-
create: true,
35-
truncate: true,
36-
})
37-
try {
38-
await res.body.pipeTo(file.writable)
39-
} finally {
40-
file.close()
41-
}
42-
return
43-
}
44-
45-
if (uri.startsWith('file://')) {
46-
const src = await Deno.open(path.fromFileUrl(uri), { read: true })
47-
const dest = await Deno.open(absPath, {
48-
write: true,
49-
create: true,
50-
truncate: true,
51-
})
52-
try {
53-
await src.readable.pipeTo(dest.writable)
54-
} finally {
55-
src.close()
56-
dest.close()
57-
}
58-
return
59-
}
60-
61-
throw new Error(`Unsupported URI scheme: ${uri}`)
62-
}
63-
6415
/**
6516
* Register file related functions to the MCP server.
6617
* @param server The MCP Server
@@ -82,9 +33,18 @@ export function registerFileFunctions(server: McpServer, rootDir: string) {
8233
},
8334
async ({ uri, filename }: { uri: string; filename: string }) => {
8435
const absPath = path.join(rootDir, filename)
85-
await uploadFromUri(uri, absPath)
36+
const fileResponse = await fetch(uri)
37+
if (fileResponse.body) {
38+
const file = await Deno.open(absPath, { write: true, create: true })
39+
await fileResponse.body.pipeTo(file.writable)
40+
}
8641
return {
87-
content: [{ type: 'text', text: 'Upload successful.' }],
42+
content: [{
43+
type: 'resource_link',
44+
uri: `file:///${filename}`,
45+
name: filename,
46+
mimeType: contentType(path.extname(absPath)),
47+
}],
8848
}
8949
},
9050
)
@@ -112,15 +72,15 @@ export function registerFileFunctions(server: McpServer, rootDir: string) {
11272
},
11373
async (uri, { filename }) => {
11474
const absPath = path.join(rootDir, ...(Array.isArray(filename) ? filename : [filename]))
115-
const mime = contentType(absPath)
75+
const mime = contentType(path.extname(absPath))
11676
const fileBytes = await Deno.readFile(absPath)
11777

11878
// Check if it's text-based
119-
if (mime && /^text\/|\/json$|\/csv$|\/javascript$|\/xml$/.test(mime)) {
79+
if (mime && /^(text\/|.*\/json$|.*\/csv$|.*\/javascript$|.*\/xml$)/.test(mime.split(';')[0])) {
12080
const text = new TextDecoder().decode(fileBytes)
12181
return { contents: [{ uri: uri.href, name: filename, mimeType: mime, text: text }] }
12282
} else {
123-
const base64 = btoa(String.fromCharCode(...fileBytes))
83+
const base64 = encodeBase64(String.fromCharCode(...fileBytes))
12484
return { contents: [{ uri: uri.href, name: filename, mimeType: mime, blob: base64 }] }
12585
}
12686
},

mcp-run-python/src/main.ts

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ const VERSION = '0.0.13'
1818
export async function main() {
1919
// Parse global flags once, then branch on subcommand
2020
const flags = parseArgs(Deno.args, {
21-
string: ['port'],
22-
default: { port: '3001', mount: false },
21+
string: ['port', 'mount'],
22+
default: { port: '3001' },
2323
})
2424
const mode = (flags._[0] as string | undefined) ?? ''
2525
const port = parseInt(flags.port as string)
26-
const mount = flags.mount as string | boolean
26+
const rawMount = flags.mount as string | undefined
27+
const mount: string | boolean = rawMount === undefined ? false : rawMount === '' ? true : rawMount
2728

2829
if (mode === 'stdio') {
2930
await runStdio(mount)
@@ -32,7 +33,7 @@ export async function main() {
3233
} else if (mode === 'sse') {
3334
runSse(port, mount)
3435
} else if (mode === 'warmup') {
35-
await warmup(mount)
36+
await warmup()
3637
} else {
3738
console.error(
3839
`\
@@ -58,8 +59,11 @@ function createServer(rootDir: string | null, mount: string | boolean): McpServe
5859
version: VERSION,
5960
},
6061
{
61-
instructions: 'Call the "run_python_code" tool with the Python code to run.',
62+
instructions: 'Call the "run_python_code" tool with the Python code to run.' +
63+
(rootDir != null ? ` Persistent storage is mounted at: "${rootDir}".` : ''),
6264
capabilities: {
65+
resources: {},
66+
tools: {},
6367
logging: {},
6468
},
6569
},
@@ -128,6 +132,36 @@ function httpSetJsonResponse(
128132
res.end()
129133
}
130134

135+
function addDirCleanupCallback(server: http.Server | StdioServerTransport, dir: string) {
136+
let cleaned = false
137+
const cleanup = () => {
138+
if (cleaned) return
139+
cleaned = true
140+
try {
141+
Deno.removeSync(dir, { recursive: true })
142+
} catch {
143+
// ignore
144+
}
145+
}
146+
if (server instanceof http.Server) {
147+
server.on('close', cleanup)
148+
} else {
149+
server.onclose = cleanup
150+
}
151+
const handleSig = () => {
152+
try {
153+
server.close(() => {})
154+
} catch {
155+
// ignore
156+
}
157+
cleanup()
158+
Deno.exit()
159+
}
160+
Deno.addSignalListener('SIGINT', handleSig)
161+
Deno.addSignalListener('SIGTERM', handleSig)
162+
addEventListener('unload', cleanup)
163+
}
164+
131165
/*
132166
* Run the MCP server using the Streamable HTTP transport
133167
*/
@@ -215,16 +249,14 @@ function runStreamableHttp(port: number, mount: string | boolean) {
215249
}
216250
})
217251

218-
// Cleanup root dir on server close
252+
// Cleanup root dir on server close and on process signals
219253
if (rootDir != null) {
220-
server.on('close', () => {
221-
Deno.removeSync(rootDir, { recursive: true })
222-
})
254+
addDirCleanupCallback(server, rootDir)
223255
}
224256

225257
server.listen(port, () => {
226258
console.log(
227-
`Running MCP Run Python version ${VERSION} with Streamable HTTP transport on port ${port}`,
259+
`Running MCP Run Python version ${VERSION} with SSE transport on port ${port}.`,
228260
)
229261
})
230262
}
@@ -275,16 +307,14 @@ function runSse(port: number, mount: string | boolean) {
275307
}
276308
})
277309

278-
// Cleanup root dir on server close
310+
// Cleanup root dir on server close and on process signals
279311
if (rootDir != null) {
280-
server.on('close', () => {
281-
Deno.removeSync(rootDir, { recursive: true })
282-
})
312+
addDirCleanupCallback(server, rootDir)
283313
}
284314

285315
server.listen(port, () => {
286316
console.log(
287-
`Running MCP Run Python version ${VERSION} with SSE transport on port ${port}`,
317+
`Running MCP Run Python version ${VERSION} with SSE transport on port ${port}.`,
288318
)
289319
})
290320
}
@@ -297,11 +327,9 @@ async function runStdio(mount: string | boolean) {
297327
const mcpServer = createServer(rootDir, mount)
298328
const transport = new StdioServerTransport()
299329

300-
// Cleanup root dir on server close
330+
// Cleanup root dir on server close and on process signals
301331
if (rootDir != null) {
302-
transport.onclose = () => {
303-
Deno.removeSync(rootDir, { recursive: true })
304-
}
332+
addDirCleanupCallback(transport, rootDir)
305333
}
306334

307335
await mcpServer.connect(transport)
@@ -310,10 +338,9 @@ async function runStdio(mount: string | boolean) {
310338
/*
311339
* Run pyodide to download packages which can otherwise interrupt the server
312340
*/
313-
async function warmup(mount?: string | boolean) {
341+
async function warmup() {
314342
console.error(
315-
`Running warmup script for MCP Run Python version ${VERSION}...` +
316-
(mount ? ` (mount: ${typeof mount === 'string' ? mount : 'enabled'})` : ''),
343+
`Running warmup script for MCP Run Python version ${VERSION}...`,
317344
)
318345
const code = `
319346
import numpy

0 commit comments

Comments
 (0)