Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions mcp_run_python/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int:
)
parser.add_argument('--verbose', action='store_true', help='Enable verbose logging')
parser.add_argument('--version', action='store_true', help='Show version and exit')
parser.add_argument('--mount-fs', action='store_true', help='Activate file persistence.')
parser.add_argument(
'mode',
choices=['stdio', 'streamable-http', 'example'],
Expand All @@ -53,6 +54,7 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int:
http_port=args.port,
dependencies=deps,
deps_log_handler=deps_log_handler,
file_persistence=args.mount_fs,
)
return return_code
else:
Expand Down
7 changes: 5 additions & 2 deletions mcp_run_python/deno/deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
},
"imports": {
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.17.5",
"@std/cli": "jsr:@std/cli@^1.0.15",
"@std/path": "jsr:@std/path@^1.0.8",
"@std/cli": "jsr:@std/cli@^1.0.21",
"@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.1.2",
"pyodide": "npm:pyodide@0.28.2",
"zod": "npm:zod@^3.24.4"
},
Expand Down
54 changes: 48 additions & 6 deletions mcp_run_python/deno/deno.lock

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

166 changes: 166 additions & 0 deletions mcp_run_python/deno/src/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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'

/**
* 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) {
// File upload
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.optional(z.string().describe('Text content of the file, if the type is "text".')),
blob: z.optional(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 from URI
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 }) => {
if (filename == null) {
throw new Deno.errors.NotFound('File not found. No filename provided.')
}
const absPath = path.join(rootDir, ...(Array.isArray(filename) ? filename : [filename]))
const mime = contentType(path.extname(absPath)) || 'application/octet-stream'
const mimeType = mime.split(';')[0] || 'application/octet-stream'
const fileBytes = await Deno.readFile(absPath)

// Check if it's text-based
if (/^(text\/|.*\/json$|.*\/csv$|.*\/javascript$|.*\/xml$)/.test(mimeType)) {
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