@@ -20,9 +20,9 @@ import {
2020} from "@modelcontextprotocol/sdk/types.js" ;
2121import { z } from "zod" ;
2222import { zodToJsonSchema } from "zod-to-json-schema" ;
23- import { readFileSync } from "fs" ;
23+ import { readFileSync , writeFileSync , appendFileSync , mkdirSync } from "fs" ;
2424import { fileURLToPath } from "url" ;
25- import { dirname , join } from "path" ;
25+ import { dirname , join , resolve , relative , isAbsolute } from "path" ;
2626
2727const __filename = fileURLToPath ( import . meta. url ) ;
2828const __dirname = dirname ( __filename ) ;
@@ -118,6 +118,21 @@ const StructuredContentSchema = {
118118 } )
119119} ;
120120
121+ const WriteFileSchema = z . object ( {
122+ path : z
123+ . string ( )
124+ . min ( 1 )
125+ . describe (
126+ "Relative file path to write under the base directory (defaults to process.cwd or MCP_WRITE_BASE_DIR)"
127+ ) ,
128+ content : z . string ( ) . default ( "" ) . describe ( "Text content to write" ) ,
129+ append : z . boolean ( ) . default ( false ) . describe ( "Append to file if true" ) ,
130+ ensureDir : z
131+ . boolean ( )
132+ . default ( true )
133+ . describe ( "Create parent directories if they do not exist" ) ,
134+ } ) ;
135+
121136enum ToolName {
122137 ECHO = "echo" ,
123138 ADD = "add" ,
@@ -129,7 +144,9 @@ enum ToolName {
129144 GET_RESOURCE_REFERENCE = "getResourceReference" ,
130145 ELICITATION = "startElicitation" ,
131146 GET_RESOURCE_LINKS = "getResourceLinks" ,
132- STRUCTURED_CONTENT = "structuredContent"
147+ STRUCTURED_CONTENT = "structuredContent" ,
148+ WRITE_FILE = "writeFile" ,
149+ WRITE_FILE_SNAKE = "write_file" ,
133150}
134151
135152enum PromptName {
@@ -524,6 +541,18 @@ export const createServer = () => {
524541 inputSchema : zodToJsonSchema ( StructuredContentSchema . input ) as ToolInput ,
525542 outputSchema : zodToJsonSchema ( StructuredContentSchema . output ) as ToolOutput ,
526543 } ,
544+ {
545+ name : ToolName . WRITE_FILE ,
546+ description :
547+ "Writes text content to a file under a safe base directory (defaults to process.cwd or MCP_WRITE_BASE_DIR)" ,
548+ inputSchema : zodToJsonSchema ( WriteFileSchema ) as ToolInput ,
549+ } ,
550+ {
551+ name : ToolName . WRITE_FILE_SNAKE ,
552+ description :
553+ "Alias of writeFile. Writes text content to a file under a safe base directory (defaults to process.cwd or MCP_WRITE_BASE_DIR)" ,
554+ inputSchema : zodToJsonSchema ( WriteFileSchema ) as ToolInput ,
555+ } ,
527556 ] ;
528557
529558 return { tools } ;
@@ -819,6 +848,44 @@ export const createServer = () => {
819848 } ;
820849 }
821850
851+ if ( name === ToolName . WRITE_FILE || name === ToolName . WRITE_FILE_SNAKE ) {
852+ const argsValidated = WriteFileSchema . parse ( args ) ;
853+ const baseDirEnv = process . env . MCP_WRITE_BASE_DIR ;
854+ const resolvedBaseDir = resolve ( baseDirEnv ? baseDirEnv : process . cwd ( ) ) ;
855+ const resolvedTargetPath = resolve ( resolvedBaseDir , argsValidated . path ) ;
856+ const rel = relative ( resolvedBaseDir , resolvedTargetPath ) ;
857+
858+ // Prevent path traversal outside base directory
859+ if ( rel . startsWith ( ".." ) || isAbsolute ( rel ) ) {
860+ throw new Error (
861+ `Invalid path: ${ argsValidated . path } . Path must stay within base directory: ${ resolvedBaseDir } `
862+ ) ;
863+ }
864+
865+ if ( argsValidated . ensureDir ) {
866+ mkdirSync ( dirname ( resolvedTargetPath ) , { recursive : true } ) ;
867+ }
868+
869+ if ( argsValidated . append ) {
870+ appendFileSync ( resolvedTargetPath , argsValidated . content , {
871+ encoding : "utf-8" ,
872+ } ) ;
873+ } else {
874+ writeFileSync ( resolvedTargetPath , argsValidated . content , {
875+ encoding : "utf-8" ,
876+ } ) ;
877+ }
878+
879+ return {
880+ content : [
881+ {
882+ type : "text" ,
883+ text : `Wrote ${ argsValidated . append ? "(appended) " : "" } ${ argsValidated . content . length } bytes to ${ resolvedTargetPath } ` ,
884+ } ,
885+ ] ,
886+ } ;
887+ }
888+
822889 throw new Error ( `Unknown tool: ${ name } ` ) ;
823890 } ) ;
824891
0 commit comments