mcp-files
Version:
Enables agents to quickly find and edit code in a codebase with surgical precision. Find symbols, edit them everywhere
67 lines (61 loc) • 18 kB
JavaScript
import S from'lodash';import {z as z$1,ZodError}from'zod';import {execSync}from'child_process';import w from'fs';import {resolve,dirname,isAbsolute,basename}from'path';import {fileURLToPath}from'url';import _t from'fast-glob';import Tt from'p-limit';import {FastMCP}from'fastmcp';var tt={PORT:et("PORT",4657),TRANSPORT:I("TRANSPORT","stdio"),DEBUG:j("DEBUG",false),OVERRIDE_S_R:j("OVERRIDE_S_R",false),COLLECT_MISMATCHES:j("COLLECT_MISMATCHES",false),CLI:false};function I(t,e){return process.env[t]??String(e)}function et(t,e){return Number.parseFloat(I(t,e))}function j(t,e){return I(t,e)==="true"}var f=tt;var D=process.env.WORKSPACE_FOLDER_PATHS||process.cwd(),b={CWD:D,REPO:resolve(dirname(fileURLToPath(import.meta.url)),".."),resolve(t,e=D){return isAbsolute(t)?t:resolve(e,t)},readFile(t,e){if(!b.exists(t)){if(S.isNil(e))throw new Error(`File not found: ${t}`);return e}return w.readFileSync(t,"utf-8")},writeFile(t,e){b.mkdirp(dirname(t)),w.writeFileSync(t,e,"utf-8");},appendNdjson(t,...e){b.mkdirp(dirname(t));let n=e.map(o=>JSON.stringify(o)).join(`
`)+`
`;w.appendFileSync(t,n,"utf-8");},mkdirp:S.memoize(t=>{b.exists(t)||w.mkdirSync(t,{recursive:true});}),ext(t){let e=t.match(/\.(\w{2,5})$/);return e?e[1]:""},isFile(t){return !!b.ext(t)},exists(t){return w.existsSync(t)},keysOf(t){return Object.keys(t)},trimLines(t){return t.replace(/^ +\n?/gm,"").trim()},clamp(t,e,n){return Math.max(e,Math.min(t,n))},truncate(t,e,n="..."){return t.length>e?t.slice(0,e)+n:t},execSync:execSync,int:it};function it(t,e){let n=Number.parseInt(t,10);return Number.isNaN(n)?e:n}var s=b;var st=s.resolve("package.json",s.REPO),{default:N}=await import(st,{with:{type:'json'}}),u={...N,version:N.version,author:N.homepage?.split("/")[3]||"unknown"};var lt=s.resolve("./logs.ndjson",s.REPO);function T(t,e,n){if(f.DEBUG)try{let o={timestamp:new Date().toISOString(),level:t,version:u.version,cwd:s.CWD,message:e,...n};s.appendNdjson(lt,o);}catch(o){console.error("Logger error:",o);}}var at={log:(t,e)=>T("LOG",t,e),info:(t,e)=>T("INFO",t,e),warn:(t,e)=>T("WARN",t,e),error:(t,e)=>T("ERROR",t,e)},E=at;var ct=g({id:"import_symbol",schema:z$1.object({module_path:z$1.string().min(1).describe('Module path to import (e.g., "lodash", "./utils", "@package/name")'),property:z$1.string().optional().describe("Only the given property from the module is dumped")}),description:"Import and inspect JavaScript/TypeScript modules ala require(), or import()",isReadOnly:true,isEnabled:f.DEBUG,fromArgs:([t,e])=>({module_path:t,property:e||void 0}),handler:async t=>{let{module_path:e,property:n}=t,r=await import(e.startsWith(".")?s.resolve(e):e);r.default&&(r=r.default);let i=n?S.get(r,n):r;if(i===void 0)throw new Error(`Property '${n}' not found in module '${e}'`);let l=[];return l.push(`=== ${e}${n?`.${n}`:""} ===`),l.push(mt(i)),l.join(`
`)}}),F=ct,A=300;function mt(t){let e=typeof t,n=[];if(e==="object"&&t!==null){let o=Object.keys(t).sort();if(o.length>0){if(S.isArray(t)){let r=typeof t[0];n.push(`${r}[${t.length}]`);}else S.isPlainObject(t)?n.push("object"):n.push(`object (${t.constructor.name})`);return o.forEach(r=>{r.startsWith("_")||n.push(`${r}: ${M(t[r])}`);}),n.join(`
`)}}return M(t)}function M(t){let e=typeof t;if(e==="function")return pt(t)?`class ${ft(t)}`:`function ${B(t)}`;if(S.isArray(t)){let n=JSON.stringify(t);return n.length<=A?n:`${typeof t[0]}[${t.length}]`}if(S.isPlainObject(t)){let n=JSON.stringify(t);return n.length<=A?n:`object (${Object.keys(t).length} properties)`}return e==="object"&&t!==null?t.constructor.name:JSON.stringify(t)}function pt(t){return t.toString().startsWith("class ")}function B(t){try{let n=t.toString().match(/\(([^)]*)\)/);return n?.[1]?`(${n[1].split(",").map(r=>r.trim().split("=")[0]?.trim()).filter(Boolean).join(", ")})`:"(...)"}catch{return "(...)"}}function ft(t){try{let e=t.toString().match(/constructor\s*\(([^)]*)\)/);return e?.[1]?`(${e[1].split(",").map(o=>o.trim().split("=")?.[0]?.trim()).filter(o=>o).join(", ")})`:B(t)}catch{return "(...)"}}var U=2,dt=z$1.object({file_path:z$1.string().min(1).describe("Path to the file"),from_line:z$1.number().int().min(1).describe("Starting line number (1-based)"),text:z$1.string().describe("Text to insert"),to_line:z$1.number().int().min(1).optional().describe("Replace up to this line number (1-based, inclusive). If omitted only inserts")}),ut=g({id:"insert_text",schema:dt,description:s.trimLines(`
Insert or replace text at precise line ranges in files
- Ideal for direct line-number operations (from code citations like 12:15:file.ts) and large files where context-heavy editing is inefficient.
- TIP: Combine with read_symbol (must use optimize: false!) to edit any symbol anywhere without knowing its file or line range!
`),isReadOnly:false,fromArgs:([t,e,n,o])=>({file_path:t,from_line:s.int(e),text:n,to_line:s.int(o)}),handler:t=>{let e=s.resolve(t.file_path),n=s.readFile(e),o=gt(n,t.from_line,t.text,t.to_line);return s.writeFile(e,o),ht(o,t.from_line,t.text,e)}}),G=ut;function gt(t,e,n,o){let r=o??e;if(r<e)throw new Error(`Invalid line range: to_line (${r}) cannot be less than from_line (${e})`);let i=t===""?[]:t.split(`
`);if(e>i.length+1)throw new Error(`from_line ${e} is beyond file length (${i.length} lines). Maximum allowed: ${i.length+1}`);if(o&&r>i.length)throw new Error(`to_line ${r} is beyond file length (${i.length} lines). Maximum allowed: ${i.length}`);let l=o?r-e+1:0,p=n.split(`
`);return i.splice(e-1,l,...p),i.join(`
`)}function ht(t,e,n,o){let r=t.split(`
`),i=n.split(`
`),l=Math.max(0,e-1-U),p=Math.min(r.length,e-1+i.length+U);return `${`=== ${l+1}:${p+1}:${o} ===`}
${r.slice(l,p).join(`
`)}`}var $t=g({id:"os_notification",schema:z$1.object({message:z$1.string().min(1).describe("The notification message to display"),title:z$1.string().optional().describe("Defaults to current project, generally omit")}),description:"Send OS notifications using native notification systems.",isReadOnly:true,fromArgs:([t,e])=>({message:t,title:e||void 0}),handler:t=>{let{message:e,title:n=basename(s.CWD)}=t,o=St(),r=o.cmd(n,e);return s.execSync(r,{stdio:"ignore"}),`Notification would have been sent via ${o.check} with title "${n}" and message "${e}"`}}),W=$t,wt=[{check:"notify-send",cmd:(t,e)=>`notify-send "${t}" "${e}"`},{check:"osascript",cmd:(t,e)=>`osascript -e 'display notification "${e}" with title "${t}"'`},{check:"powershell",cmd:(t,e)=>`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; \\$notify = New-Object System.Windows.Forms.NotifyIcon; \\$notify.Icon = [System.Drawing.SystemIcons]::Information; \\$notify.BalloonTipTitle = '${t}'; \\$notify.BalloonTipText = '${e}'; \\$notify.Visible = \\$true; \\$notify.ShowBalloonTip(5000); Start-Sleep -Seconds 2; \\$notify.Dispose()"`},{check:"powershell.exe",cmd:(t,e)=>`powershell.exe -Command "Add-Type -AssemblyName System.Windows.Forms; \\$notify = New-Object System.Windows.Forms.NotifyIcon; \\$notify.Icon = [System.Drawing.SystemIcons]::Information; \\$notify.BalloonTipTitle = '${t}'; \\$notify.BalloonTipText = '${e}'; \\$notify.Visible = \\$true; \\$notify.ShowBalloonTip(5000); Start-Sleep -Seconds 2; \\$notify.Dispose()"`},{check:"wsl-notify-send.exe",cmd:(t,e)=>`wsl-notify-send.exe --category "${t}" "${e}"`}],St=S.memoize(()=>{for(let t of wt)try{return s.execSync(`command -v ${t.check}`,{stdio:"ignore"}),t}catch{}throw new Error("No notification method available. Install notify-send, osascript, powershell, or wsl-notify-send")});var Et=32,Rt=10*1024*1024,Ot=2e3,jt=3e4,It=200,Ct=20,z=5,Nt=["d.ts","ts","tsx","js","jsx","mjs","cjs","cts","java","cs","cpp","c","h","hpp","cc","go","rs","php","swift","scss","css","less","graphql","gql","prisma","proto"],kt=["node_modules",".git","test","tests","examples","runtime"],Pt=["dist","build","out"],Lt=["bin","scripts"],vt=["*.test.*","*.spec.*","_*","*.min.*"],Dt=/\b(class|interface|type|function|enum|namespace|module|model|declare|abstract|const|extends|implements)\b/gi,At=g({id:"read_symbol",schema:z$1.object({symbols:z$1.array(z$1.string().min(1)).describe("Symbol name(s) to find (functions, classes, types, etc.), case-sensitive, supports * for wildcard"),file_paths:z$1.array(z$1.string().min(1)).optional().describe('File paths to search (supports relative and glob). Defaults to "." (current directory). IMPORTANT: Be specific with paths when possible, minimize broad patterns like "node_modules/**" to avoid mismatches'),limit:z$1.number().optional().describe(`Maximum number of results to return. Defaults to ${z}`),optimize:z$1.boolean().optional().describe("Unless explicitly false, this tool will strip comments and spacing to preserve AI's context window, omit unless you REALLY it unchanged (default: true)")}),description:"Find and extract symbol block by name from files, supports a lot of file formats (like TS, JS, GraphQL, CSS and most that use braces for blocks). Uses streaming with concurrency control for better performance",isReadOnly:true,fromArgs:([t,...e])=>({symbols:t.split(","),file_paths:e.length?e:void 0}),handler:async t=>{let{symbols:e,file_paths:n=[],limit:o=z,optimize:r=true}=t;n.length||n.push(".");let i=n.map(Mt),l=[],p=0,a=Ct*e.length;try{for await(let m of Bt(e,i))if(p++,l.push(m),l.length>=a)break}catch(m){if(!l.length)throw m}if(!l.length)throw f.COLLECT_MISMATCHES&&Vt({symbols:e,file_paths:n,limit:o}),new Error(`Failed to find the \`${e.join(", ")}\` symbol(s) in any files`);let c=l.sort((m,d)=>d.score-m.score).slice(0,o*e.length).map(m=>Ht(m,r)).join(`
`);return p>l.length&&(c+=`
--- Showing ${l.length} matches out of ${p} ---`),c}});function Mt(t){let e=`.{${Nt.join(",")}}`;if(t==="*"||t==="*.*")return `*${e}`;if(/[/*\w]\.\w+$/.test(t))return t;let n=/[*?[{]/.test(t);return t=t.replace(/node_modules\/(\w+)/g,"node_modules/{@types/,}$1"),t.endsWith("/")?`${t}**/*${e}`:n?`${t}${e}`:/[/\\.]/.test(t)?t:`${t}/**/*${e}`}function Ft(t){let e=kt.filter(i=>!t.some(l=>l.includes(`${i}/`))),n=Pt.filter(i=>!t.some(l=>l.includes(`${i}/`))),o=Lt.filter(i=>!t.some(l=>l.includes(`*/**/${i}`))).map(i=>`*/**/${i}`),r=[`!**/{${vt.join(",")}}`];return e.length&&r.push(`!{${e.join(",")}}/**`),n.forEach(i=>{r.push(`!${i}/**`);}),o.length&&r.push(`!{${o.join(",")}}`),r}async function*Bt(t,e){let n=Tt(Et),o=false,r=Ft(e),i=[...e,...r],l=_t.stream(i,{cwd:s.CWD,onlyFiles:true,absolute:false,stats:true,suppressErrors:true,deep:4}),p=new Set,a=0;try{for await(let c of l){if(++a===Ot||o)break;if(c.stats&&c.stats.size>Rt)continue;let m=a,d=n(async()=>{if(o)return [];try{let O=await w.promises.readFile(s.resolve(c.path),"utf8");return o?[]:Ut(O,t,c.path,m)}catch{return []}});p.add(d);let Q=await d;p.delete(d);for(let O of Q)yield O;}}finally{o=true,await Promise.allSettled([...p]);}}function Ut(t,e,n,o){let r=[];if(!t.includes(`
`))return r;for(let i of Gt(t,e)){let p=t.substring(0,i.index).split(`
`).length,[a]=i;if(a.length>jt)continue;let c=p+a.split(`
`).length-1,m=Xt(a,n);r.push({text:a,startLine:p,endLine:c,path:n,score:m,index:i.index,fileIndex:o});}return r}function Gt(t,e){let n=Wt(e);return n.lastIndex=0,t.matchAll(n)||[]}var Wt=S.memoize(t=>{let e=X(t);return new RegExp(`^(?:\\s*/[/*][^
]*
)*([ ]*).{0,${It}}(?<![([.'"])(?:${e})(?![.'")]]).*\\s*\\{(?:\r?
\\1\\s+.*)+[^}]*\\}`,"mg")},X);function X(t){return t.map(zt).join("|")}function zt(t){let e=S.escapeRegExp(t);return /^[\w*]/.test(t)&&(e=`\\b${e}`),/[\w*]$/.test(t)&&(e+="\\b"),e.replace(/\\\*/g,"\\w*")}function Xt(t,e){let n=0,o=t.split(`
`);n+=o.length*2;let r=o[0].match(Dt)||[];n+=r.length*1e3;let i=e.split("/").length;return n-=i*10,e.endsWith(".d.ts")&&(n+=100),Math.round(n)}function Ht(t,e=false){let n=`${t.startLine}:${t.endLine}:${t.path}`,{text:o}=t,{length:r}=o;if(e&&(o=Jt(o)),f.CLI&&f.DEBUG){let i=Math.round(process.uptime()*1e3);n+=` | Chars: ${r}->${o.length} | Index: ${t.index}-${t.index+r} | File: #${t.fileIndex} | Score: ${t.score} | Time: ${i}ms`;}return `=== ${n} ===
${o}`}function Jt(t){let e=t.replace(/([^:])\/\/.*$/gm,"$1").replace(/\/\*[\s\S]*?\*\//g,"").replace(/"""[\s\S]*?"""/g,"").replace(/^\s*#.*/gm,"").replace(/[ \t]+$/gm,"").replace(/^\s*$/gm,"").replace(/\n\n+/g,`
`),n=e.split(`
`),o=n.filter(a=>a.trim());if(!o.length)return e.trim();let r=o.map(a=>a.match(/^\s*/)?.[0].length||0),i=Math.min(...r),l=r.map(a=>a-i).filter(a=>a>0),p=l.length?Math.min(...l):2;return n.map(a=>{if(!a.trim())return "";let c=a.match(/^\s*/)?.[0]||"",m=a.slice(c.length),d=c.length-i;return d>0?" ".repeat(Math.floor(d/p))+m:m}).join(`
`).trim()}function Vt(t){try{let e=s.resolve("./mismatches.ndjson",s.REPO),n={...t,cwd:s.CWD,timestamp:new Date().toISOString()};s.appendNdjson(e,n);}catch{}}var J=At;var V=2,K="search_replace",Zt=g({id:K,name:`${f.OVERRIDE_S_R?"":"better_"}${K}`,schema:z$1.object({file_path:z$1.string().min(1).describe("Path to the file (supports relative and absolute paths)"),old_string:z$1.string().min(1).describe("Exact text to replace (must be unique in file)"),new_string:z$1.string().describe("Replacement text"),allow_multiple_matches:z$1.boolean().optional().describe("Allow multiple matches to be replaced. If false, throws error when multiple matches found (default: true)")}),description:"Search and replace with intelligent whitespace handling and automation-friendly multiple match resolution. Tries exact match first, falls back to flexible whitespace matching only when no matches found.",isReadOnly:false,isEnabled:f.DEBUG,fromArgs:([t,e,n])=>({file_path:t,old_string:e,new_string:n}),handler:t=>{let{file_path:e,old_string:n,new_string:o,allow_multiple_matches:r=true}=t,i=s.resolve(e),l=s.readFile(i);if(n.includes(o))throw new Error(`Redundant replacement: old_string already contains new_string. Old: "${n}", New: "${o}"`);let p=[n,qt(n)];for(let a of p){let c=l.split(a),m=c.length-1;if(!m)continue;if(m>1&&!r)throw new Error(`Multiple matches found (${m}) for "${n}" in ${e}. Set allow_multiple_matches=true to allow replacing first occurrence, or make your search string more specific.`);let d=c.join(o);return s.writeFile(i,d),Yt(l,d)}throw new Error(`Could not find the specified text in ${e}`)}});function qt(t){return new RegExp(S.escapeRegExp(t).replace(/\s+/g,"\\s+").replace(/^\s*/,"\\s*").replace(/\s*$/,"\\s*"),"gm")}var Z=Zt;function Yt(t,e){let n=t.split(`
`),o=e.split(`
`),r=-1,i=-1;for(let c=0;c<Math.max(n.length,o.length);c++)n[c]!==o[c]&&(r===-1&&(r=c),i=c);if(r===-1)throw new Error("Could not generate a diff for changes to the file");let l=Math.max(0,r-V),p=Math.min(n.length-1,i+V),a=[];for(let c=l;c<=p;c++){let m=n[c]||"",d=o[c]||"";c<r||c>i?a.push(` ${m}`):m!==d?(m&&a.push(`- ${m}`),d&&a.push(`+ ${d}`)):a.push(` ${m}`);}return `The following diff was applied to the file:
\`\`\`
${a.join(`
`)}
\`\`\``}var te=g({id:"utils_debug",schema:z$1.object({}),description:s.trimLines(`
Get debug information about available tools and environment.
- ${u.name} version: ${u.version}
`),isReadOnly:true,isEnabled:f.DEBUG,fromArgs:()=>({}),handler:(t,e)=>({...t,processEnv:process.env,argv:process.argv,env:f,context:e,version:u.version,CWD:s.CWD,REPO:s.REPO})}),q=te;function g(t){return {isResource:false,isReadOnly:false,isEnabled:true,name:t.name??t.id,...t}}var ee={read_symbol:J,import_symbol:F,search_replace:Z,insert_text:G,os_notification:W,utils_debug:q},$=ee;var re={isCommand:t=>t&&t in $,async run(t){let e=t.shift(),n=$[e];if(!n)throw new Error(`Unknown command: ${e}`);f.CLI=true;let o=await this.runTool(n,n.fromArgs(t));console.log(o);},async runTool(t,e){try{e=t.schema.parse(e);let n=await t.handler(e);return S.isString(n)?n:JSON.stringify(n)}catch(n){let o={name:t.name,args:e,error:n.message};throw n instanceof ZodError&&(o.issues=n.issues.map(r=>`${r.path.join(".")}: ${r.message}`).join(", ")),E.error("Tool execution failed",o),n}}},y=re;async function se(){let t=new FastMCP({name:`${u.author}/${u.name}`,version:u.version});for(let e of Object.values($))e.isEnabled&&(e.isResource?t.addResource({uri:`resource://${e.name}`,name:e.description,mimeType:"text/plain",load:()=>y.runTool(e,[]).then(n=>({text:n}))}):t.addTool({annotations:{openWorldHint:false,readOnlyHint:e.isReadOnly,title:e.name},name:e.name,description:e.description,parameters:e.schema,execute:n=>y.runTool(e,n)}));f.TRANSPORT==="http"?await t.start({transportType:"httpStream",httpStream:{port:f.PORT}}):(await t.start({transportType:"stdio"}),E.log("Started new server",{transport:f.TRANSPORT}));}var Y={start:se};var R=process.argv.slice(2);if(R.length===0)await Y.start();else if(y.isCommand(R[0]))await y.run(R);else if(R[0]==="--check")process.exit(0);else {let t=u.name;console.log(`${u.author}/${t} ${u.version}
${u.description}
Server Usage:
${t} # Run MCP server with stdio transport
TRANSPORT=http ${t} # Run MCP server with HTTP transport
CLI Usage:
${t} read_symbol <symbol[,symbol2,...]> <file1> [file2...] # Find code blocks by symbol name(s)
${t} import_symbol <module_path> [property] # Inspect modules and imports
${t} search_replace <file> <old_text> <new_text> # Search and replace with whitespace handling
${t} insert_text <file> <from_line> <text> [to_line] # Insert or replace text at line range (1-based)
${t} os_notification <message> [title] # Send OS notifications (title defaults to current directory)
${t} utils_debug # Get debug information
Examples:
${t} read_symbol "ToolConfig" src/types.ts
${t} read_symbol "User,UserService,UserInterface" src/
${t} read_symbol "get*,process*" src/
${t} import_symbol lodash get
${t} search_replace src/app.ts "old code" "new code"
${t} insert_text src/app.ts 10 "console.log('debug')" # Insert at line 10
${t} insert_text src/app.ts 10 "new code" 12 # Replace lines 10-12
${t} os_notification "Build complete"
${t} utils_debug
`),process.exit(0);}export{y as cli,$ as tools};