diff --git a/mcp-run-python/README.md b/mcp-run-python/README.md
index 4492f1eb7..d01384cc7 100644
--- a/mcp-run-python/README.md
+++ b/mcp-run-python/README.md
@@ -12,7 +12,7 @@ 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|sse|warmup]
+ jsr:@pydantic/mcp-run-python [stdio|sse|warmup] [--mount local_path:pyodide_path]
```
where:
@@ -29,6 +29,9 @@ where:
running the server as an HTTP server to connect locally or remotely
- `warmup` will run a minimal Python script to download and cache the Python standard library. This is also useful to
check the server is running correctly.
+- `--mount local_path:pyodide_path` (optional) mounts a local filesystem directory to the Pyodide filesystem, allowing
+ Python code to read and write files. Files are automatically synced back to the local filesystem after successful
+ execution.
Here's an example of using `@pydantic/mcp-run-python` with Pydantic AI:
diff --git a/mcp-run-python/deno.json b/mcp-run-python/deno.json
index b84e0546c..89ea7853c 100644
--- a/mcp-run-python/deno.json
+++ b/mcp-run-python/deno.json
@@ -15,6 +15,7 @@
"imports": {
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.15.1",
"@std/cli": "jsr:@std/cli@^1.0.15",
+ "@std/fs": "jsr:@std/fs@^1.0.8",
"@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:pyodide@0.27.6",
diff --git a/mcp-run-python/deno.lock b/mcp-run-python/deno.lock
index 0bd868073..a29b34283 100644
--- a/mcp-run-python/deno.lock
+++ b/mcp-run-python/deno.lock
@@ -3,8 +3,11 @@
"specifiers": {
"jsr:@std/cli@*": "1.0.15",
"jsr:@std/cli@^1.0.15": "1.0.15",
+ "jsr:@std/fs@^1.0.8": "1.0.19",
+ "jsr:@std/internal@^1.0.9": "1.0.10",
"jsr:@std/path@*": "1.0.8",
"jsr:@std/path@^1.0.8": "1.0.8",
+ "jsr:@std/path@^1.1.1": "1.1.1",
"npm:@modelcontextprotocol/sdk@^1.15.1": "1.15.1_express@5.1.0_zod@3.24.2",
"npm:@types/node@*": "22.12.0",
"npm:@types/node@22.12.0": "22.12.0",
@@ -16,8 +19,23 @@
"@std/cli@1.0.15": {
"integrity": "e79ba3272ec710ca44d8342a7688e6288b0b88802703f3264184b52893d5e93f"
},
+ "@std/fs@1.0.19": {
+ "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06",
+ "dependencies": [
+ "jsr:@std/path@^1.1.1"
+ ]
+ },
+ "@std/internal@1.0.10": {
+ "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7"
+ },
"@std/path@1.0.8": {
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
+ },
+ "@std/path@1.1.1": {
+ "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76",
+ "dependencies": [
+ "jsr:@std/internal"
+ ]
}
},
"npm": {
@@ -914,6 +932,7 @@
"workspace": {
"dependencies": [
"jsr:@std/cli@^1.0.15",
+ "jsr:@std/fs@^1.0.8",
"jsr:@std/path@^1.0.8",
"npm:@modelcontextprotocol/sdk@^1.15.1",
"npm:pyodide@0.27.6",
diff --git a/mcp-run-python/src/filesystem.ts b/mcp-run-python/src/filesystem.ts
new file mode 100644
index 000000000..f315a0701
--- /dev/null
+++ b/mcp-run-python/src/filesystem.ts
@@ -0,0 +1,252 @@
+///
+
+import type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js'
+import { walk } from '@std/fs/walk'
+import { relative } from '@std/path'
+
+export interface FileInfo {
+ type: 'text' | 'binary'
+ content: string
+}
+
+export interface MountPathInfo {
+ localPath: string
+ pyodidePath: string
+}
+
+// Basic interface for Pyodide functionality used in filesystem operations
+interface PyodideInterface {
+ pyimport: (name: string) => unknown
+}
+
+// Interface for Python pathlib module
+interface PathlibModule {
+ Path: (path: string) => PathlibPath
+}
+
+interface PathlibPath {
+ mkdir: (options?: { parents?: boolean; exist_ok?: boolean }) => void
+ parent: PathlibPath
+ write_text: (content: string) => void
+ write_bytes: (data: unknown) => void
+ exists: () => boolean
+ rglob: (pattern: string) => PathlibPath[]
+ is_file: () => boolean
+ relative_to: (other: PathlibPath) => PathlibPath
+ toString: () => string
+ read_text: (options?: { encoding?: string }) => string
+ read_bytes: () => unknown
+}
+
+// Interface for Python base64 module
+interface Base64Module {
+ b64decode: (data: string) => unknown
+ b64encode: (data: unknown) => { decode: (encoding: string) => string }
+}
+
+/**
+ * Parse mount path string in format "local_path:pyodide_path"
+ * Returns null if format is invalid
+ */
+export function parseMountPath(mountPath: string): MountPathInfo | null {
+ const [localPath, pyodidePath] = mountPath.split(':')
+ if (localPath && pyodidePath) {
+ return { localPath, pyodidePath }
+ }
+ return null
+}
+
+/**
+ * Handle filesystem mounting with path parsing and validation
+ */
+export async function handleMount(
+ pyodide: PyodideInterface,
+ mountPath: string,
+ log: (level: LoggingLevel, data: string) => void,
+) {
+ const mountInfo = parseMountPath(mountPath)
+ if (mountInfo) {
+ await mountFilesToPyodide(pyodide, mountInfo.localPath, mountInfo.pyodidePath, log)
+ } else {
+ log('warning', 'Invalid mount path format. Use: local_path:pyodide_path')
+ }
+}
+
+/**
+ * Handle filesystem sync back with path parsing and validation
+ */
+export async function handleSyncBack(
+ pyodide: PyodideInterface,
+ mountPath: string,
+ log: (level: LoggingLevel, data: string) => void,
+) {
+ const mountInfo = parseMountPath(mountPath)
+ if (mountInfo) {
+ try {
+ await syncFilesFromPyodide(pyodide, mountInfo.pyodidePath, mountInfo.localPath, log)
+ } catch (error) {
+ log('warning', `Failed to sync files back to ${mountInfo.localPath}: ${error}`)
+ }
+ }
+}
+
+/**
+ * Read all files from a local directory and return them as a map
+ * with relative paths as keys and file info as values
+ */
+export async function readLocalDirectory(localPath: string): Promise