Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dafe12e
Update pyodide
Aug 29, 2025
01584da
Run Make
Aug 29, 2025
5f9f4f0
Merge branch 'main' into update-pyodide-version
Aug 29, 2025
b4ea0e5
Run Make
Aug 29, 2025
8c54a1f
Merge branch 'main' into update-pyodide-version
Aug 30, 2025
e3c2f3b
Initial commit
Aug 30, 2025
ee18257
Update mcp server package version
Aug 30, 2025
5c65995
Merge branch 'update-mcp-server' into add-file-support
Aug 30, 2025
d26dab5
Added file related MCP function
Aug 31, 2025
82c377a
Merge branch 'main' into update-pyodide-version
Aug 31, 2025
74bef11
Merge branch 'main' into add-file-support
Aug 31, 2025
26777a0
Add upload tests
Sep 1, 2025
9f19fe1
Merge branch 'main' into update-pyodide-version
Sep 1, 2025
980e9b9
Merge branch 'main' into add-file-support
Sep 1, 2025
77158f3
Remove comments
Sep 1, 2025
ecb110c
Merge branch 'main' into update-pyodide-version
Sep 1, 2025
bf99434
Add download test cases
Sep 1, 2025
792d080
Merge branch 'main' into add-file-support
Sep 1, 2025
f3fa6b5
Add code tests with file
Sep 2, 2025
db14769
Remove other comment
Sep 2, 2025
d45cd03
Merge branch 'main' into update-pyodide-version
Sep 2, 2025
25ef73b
Merge branch 'main' into add-file-support
Sep 2, 2025
3e3a3b4
Merge branch 'update-pyodide-version' into add-file-support
Sep 2, 2025
6ad82c6
Add tests which run python code. Small bug left.
Sep 2, 2025
01cb5b2
Finish PR
Sep 2, 2025
afdf978
Add test for `retrieve_file`
Sep 2, 2025
13e74f4
Update README
Sep 2, 2025
5856737
Add delete file tool
Sep 2, 2025
bb690bb
Add direct upload tool
Sep 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions mcp-run-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@ The server can be run with `deno` installed using:

```bash
deno run \
-N -R=node_modules -W=node_modules --node-modules-dir=auto \
jsr:@pydantic/mcp-run-python [stdio|streamable_http|sse|warmup]
-N -R=node_modules,/tmp -W=node_modules,/tmp --node-modules-dir=auto \
jsr:@pydantic/mcp-run-python [stdio|streamable_http|sse|warmup] [--mount ./storage/]
```

where:

- `-N -R=node_modules -W=node_modules` (alias of `--allow-net --allow-read=node_modules --allow-write=node_modules`)
allows network access and read+write access to `./node_modules`. These are required so pyodide can download and cache
the Python standard library and packages
- `-N -R=node_modules,/tmp -W=node_modules,/tmp` (alias of
`--allow-net --allow-read=node_modules,/tmp --allow-write=node_modules,/tmp`) allows network access and read+write
access to `./node_modules` and `/tmp`. These are required so pyodide can download and cache the Python standard
library and packages
- `--node-modules-dir=auto` tells deno to use a local `node_modules` directory
- `--mount ./storage`: Optionally, mount a directory to `/home/pyodide/storage` for persist file between pyodide runs.
File can be uploaded and retrieve using the `upload_file_from_uri`and `retrieve_file` tools respectively.
- `stdio` runs the server with the
[Stdio MCP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio) — suitable for
running the process as a subprocess locally
Expand Down Expand Up @@ -52,8 +55,8 @@ server = MCPServerStdio('deno',
args=[
'run',
'-N',
'-R=node_modules',
'-W=node_modules',
'-R=node_modules,/tmp',
'-W=node_modules,/tmp',
'--node-modules-dir=auto',
'jsr:@pydantic/mcp-run-python',
'stdio',
Expand Down
6 changes: 4 additions & 2 deletions mcp-run-python/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
"imports": {
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.17.4",
"@std/cli": "jsr:@std/cli@^1.0.15",
"@std/encoding": "jsr:@std/encoding@^1.0.10",
"@std/fs": "jsr:@std/fs@^1.0.19",
"@std/media-types": "jsr:@std/media-types@^1.1.0",
"@std/path": "jsr:@std/path@^1.0.8",
// do NOT upgrade above this version until there is a workaround for https://github.com/pyodide/pyodide/pull/5621
"pyodide": "npm:[email protected]",
"pyodide": "npm:[email protected]",
"zod": "npm:zod@^3.24.2"
},
"fmt": {
Expand Down
67 changes: 51 additions & 16 deletions mcp-run-python/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

167 changes: 167 additions & 0 deletions mcp-run-python/src/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import * as path from '@std/path'
import { exists } from '@std/fs/exists'
import { contentType } from '@std/media-types'
import { type McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
import z from 'zod'
import { decodeBase64, encodeBase64 } from '@std/encoding/base64'

/**
* Returns the temporary directory in the local filesystem for file persistence.
*/
export function createRootDir(): string {
return Deno.makeTempDirSync({ prefix: 'mcp_run_python' })
}

/**
* Register file related functions to the MCP server.
* @param server The MCP Server
* @param rootDir Directory in the local file system to read/write to.
*/
export function registerFileFunctions(server: McpServer, rootDir: string) {
server.registerTool('upload_file', {
title: 'Upload file.',
description: 'Ingest a file from the given object. Returns a link to the resource that was created.',
inputSchema: {
type: z.union([z.literal('text'), z.literal('bytes')]),
filename: z.string().describe('Name of the file to write.'),
text: z.nullable(z.string().describe('Text content of the file, if the type is "text".')),
blob: z.nullable(z.string().describe('Base 64 encoded content of the file, if the type is "bytes".')),
},
}, async ({ type, filename, text, blob }) => {
const absPath = path.join(rootDir, filename)
if (type === 'text') {
if (text == null) {
return { content: [{ type: 'text', text: "Type is 'text', but no text provided." }], isError: true }
}
await Deno.writeTextFile(absPath, text)
} else {
if (blob == null) {
return { content: [{ type: 'text', text: "Type is 'bytes', but no blob provided." }], isError: true }
}
await Deno.writeFile(absPath, decodeBase64(blob))
}
return {
content: [{
type: 'resource_link',
uri: `file:///${filename}`,
name: filename,
mimeType: contentType(path.extname(absPath)),
}],
}
})
// File Upload
server.registerTool(
'upload_file_from_uri',
{
title: 'Upload file from URI',
description: 'Ingest a file by URI and store it. Returns a canonical URL.',
inputSchema: {
uri: z.string().url().describe('file:// or https:// style URL'),
filename: z
.string()
.describe('The name of the file to write.'),
},
},
async ({ uri, filename }: { uri: string; filename: string }) => {
const absPath = path.join(rootDir, filename)
const fileResponse = await fetch(uri)
if (fileResponse.body) {
const file = await Deno.open(absPath, { write: true, create: true })
await fileResponse.body.pipeTo(file.writable)
}
return {
content: [{
type: 'resource_link',
uri: `file:///${filename}`,
name: filename,
mimeType: contentType(path.extname(absPath)),
}],
}
},
)

// Register all the files in the local directory as resources
server.registerResource(
'read-file',
new ResourceTemplate('file:///{filename}', {
list: async (_extra) => {
const resources = []
for await (const dirEntry of Deno.readDir(rootDir)) {
if (!dirEntry.isFile) continue
resources.push({
uri: `file:///${dirEntry.name}`,
name: dirEntry.name,
mimeType: contentType(path.extname(dirEntry.name)),
})
}
return { resources: resources }
},
}),
{
title: 'Read file.',
description: 'Read file from persistent storage',
},
async (uri, { filename }) => {
const absPath = path.join(rootDir, ...(Array.isArray(filename) ? filename : [filename]))
const mime = contentType(path.extname(absPath))
const fileBytes = await Deno.readFile(absPath)

// Check if it's text-based
if (mime && /^(text\/|.*\/json$|.*\/csv$|.*\/javascript$|.*\/xml$)/.test(mime.split(';')[0])) {
const text = new TextDecoder().decode(fileBytes)
return { contents: [{ uri: uri.href, mimeType: mime, text: text }] }
} else {
const base64 = encodeBase64(fileBytes)
return { contents: [{ uri: uri.href, mimeType: mime, blob: base64 }] }
}
},
)

// This functions only checks if the file exits
// Download happens through the registered resource
server.registerTool('retrieve_file', {
title: 'Retrieve a file',
description: 'Retrieve a file from the persistent file store.',
inputSchema: { filename: z.string().describe('The name of the file to read.') },
}, async ({ filename }) => {
const absPath = path.join(rootDir, filename)
if (await exists(absPath, { isFile: true })) {
return {
content: [{
type: 'resource_link',
uri: `file:///${filename}`,
name: filename,
mimeType: contentType(path.extname(absPath)),
}],
}
} else {
return {
content: [{ 'type': 'text', 'text': `Failed to retrieve file ${filename}. File not found.` }],
isError: true,
}
}
})

// File deletion
server.registerTool('delete_file', {
title: 'Delete a file',
description: 'Delete a file from the persistent file store.',
inputSchema: { filename: z.string().describe('The name of the file to delete.') },
}, async ({ filename }) => {
const absPath = path.join(rootDir, filename)
if (await exists(absPath, { isFile: true })) {
await Deno.remove(absPath)
return {
content: [{
type: 'text',
text: `${filename} deleted successfully`,
}],
}
} else {
return {
content: [{ 'type': 'text', 'text': `Failed to delete file ${filename}. File not found.` }],
isError: true,
}
}
})
}
Loading