commit
fc7183983c
4 changed files with 176 additions and 3 deletions
19
package-lock.json
generated
19
package-lock.json
generated
|
|
@ -1523,6 +1523,12 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/diff": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/express": {
|
"node_modules/@types/express": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
|
||||||
|
|
@ -2258,6 +2264,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
|
||||||
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg=="
|
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.6",
|
"version": "16.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.6.tgz",
|
||||||
|
|
@ -4983,6 +4997,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/aws-kb-retrieval-server": {
|
"src/aws-kb-retrieval-server": {
|
||||||
|
"name": "@modelcontextprotocol/server-aws-kb-retrieval",
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -5167,7 +5182,8 @@
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "1.0.1",
|
"@modelcontextprotocol/sdk": "0.5.0",
|
||||||
|
"diff": "^5.1.0",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"zod-to-json-schema": "^3.23.5"
|
"zod-to-json-schema": "^3.23.5"
|
||||||
},
|
},
|
||||||
|
|
@ -5175,6 +5191,7 @@
|
||||||
"mcp-server-filesystem": "dist/index.js"
|
"mcp-server-filesystem": "dist/index.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/diff": "^5.0.9",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,30 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio
|
||||||
- `path` (string): File location
|
- `path` (string): File location
|
||||||
- `content` (string): File content
|
- `content` (string): File content
|
||||||
|
|
||||||
|
- **edit_file**
|
||||||
|
- Make selective edits using advanced pattern matching and formatting
|
||||||
|
- Features:
|
||||||
|
- Line-based and multi-line content matching
|
||||||
|
- Whitespace normalization with indentation preservation
|
||||||
|
- Fuzzy matching with confidence scoring
|
||||||
|
- Multiple simultaneous edits with correct positioning
|
||||||
|
- Indentation style detection and preservation
|
||||||
|
- Git-style diff output with context
|
||||||
|
- Preview changes with dry run mode
|
||||||
|
- Failed match debugging with confidence scores
|
||||||
|
- Inputs:
|
||||||
|
- `path` (string): File to edit
|
||||||
|
- `edits` (array): List of edit operations
|
||||||
|
- `oldText` (string): Text to search for (can be substring)
|
||||||
|
- `newText` (string): Text to replace with
|
||||||
|
- `dryRun` (boolean): Preview changes without applying (default: false)
|
||||||
|
- `options` (object): Optional formatting settings
|
||||||
|
- `preserveIndentation` (boolean): Keep existing indentation (default: true)
|
||||||
|
- `normalizeWhitespace` (boolean): Normalize spaces while preserving structure (default: true)
|
||||||
|
- `partialMatch` (boolean): Enable fuzzy matching (default: true)
|
||||||
|
- Returns detailed diff and match information for dry runs, otherwise applies changes
|
||||||
|
- Best Practice: Always use dryRun first to preview changes before applying them
|
||||||
|
|
||||||
- **create_directory**
|
- **create_directory**
|
||||||
- Create new directory or ensure it exists
|
- Create new directory or ensure it exists
|
||||||
- Input: `path` (string)
|
- Input: `path` (string)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import path from "path";
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||||
|
import { diffLines, createTwoFilesPatch } from 'diff';
|
||||||
|
|
||||||
// Command line argument parsing
|
// Command line argument parsing
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
@ -106,6 +107,17 @@ const WriteFileArgsSchema = z.object({
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const EditOperation = z.object({
|
||||||
|
oldText: z.string().describe('Text to search for - must match exactly'),
|
||||||
|
newText: z.string().describe('Text to replace with')
|
||||||
|
});
|
||||||
|
|
||||||
|
const EditFileArgsSchema = z.object({
|
||||||
|
path: z.string(),
|
||||||
|
edits: z.array(EditOperation),
|
||||||
|
dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
|
||||||
|
});
|
||||||
|
|
||||||
const CreateDirectoryArgsSchema = z.object({
|
const CreateDirectoryArgsSchema = z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
});
|
});
|
||||||
|
|
@ -202,6 +214,104 @@ async function searchFiles(
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// file editing and diffing utilities
|
||||||
|
function normalizeLineEndings(text: string): string {
|
||||||
|
return text.replace(/\r\n/g, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
|
||||||
|
// Ensure consistent line endings for diff
|
||||||
|
const normalizedOriginal = normalizeLineEndings(originalContent);
|
||||||
|
const normalizedNew = normalizeLineEndings(newContent);
|
||||||
|
|
||||||
|
return createTwoFilesPatch(
|
||||||
|
filepath,
|
||||||
|
filepath,
|
||||||
|
normalizedOriginal,
|
||||||
|
normalizedNew,
|
||||||
|
'original',
|
||||||
|
'modified'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyFileEdits(
|
||||||
|
filePath: string,
|
||||||
|
edits: Array<{oldText: string, newText: string}>,
|
||||||
|
dryRun = false
|
||||||
|
): Promise<string> {
|
||||||
|
// Read file content and normalize line endings
|
||||||
|
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
|
||||||
|
|
||||||
|
// Apply edits sequentially
|
||||||
|
let modifiedContent = content;
|
||||||
|
for (const edit of edits) {
|
||||||
|
const normalizedOld = normalizeLineEndings(edit.oldText);
|
||||||
|
const normalizedNew = normalizeLineEndings(edit.newText);
|
||||||
|
|
||||||
|
// If exact match exists, use it
|
||||||
|
if (modifiedContent.includes(normalizedOld)) {
|
||||||
|
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, try line-by-line matching with flexibility for whitespace
|
||||||
|
const oldLines = normalizedOld.split('\n');
|
||||||
|
const contentLines = modifiedContent.split('\n');
|
||||||
|
let matchFound = false;
|
||||||
|
|
||||||
|
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
|
||||||
|
const potentialMatch = contentLines.slice(i, i + oldLines.length);
|
||||||
|
|
||||||
|
// Compare lines with normalized whitespace
|
||||||
|
const isMatch = oldLines.every((oldLine, j) => {
|
||||||
|
const contentLine = potentialMatch[j];
|
||||||
|
return oldLine.trim() === contentLine.trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
// Preserve original indentation of first line
|
||||||
|
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
|
||||||
|
const newLines = normalizedNew.split('\n').map((line, j) => {
|
||||||
|
if (j === 0) return originalIndent + line.trimStart();
|
||||||
|
// For subsequent lines, try to preserve relative indentation
|
||||||
|
const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
|
||||||
|
const newIndent = line.match(/^\s*/)?.[0] || '';
|
||||||
|
if (oldIndent && newIndent) {
|
||||||
|
const relativeIndent = newIndent.length - oldIndent.length;
|
||||||
|
return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
|
||||||
|
contentLines.splice(i, oldLines.length, ...newLines);
|
||||||
|
modifiedContent = contentLines.join('\n');
|
||||||
|
matchFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchFound) {
|
||||||
|
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create unified diff
|
||||||
|
const diff = createUnifiedDiff(content, modifiedContent, filePath);
|
||||||
|
|
||||||
|
// Format diff with appropriate number of backticks
|
||||||
|
let numBackticks = 3;
|
||||||
|
while (diff.includes('`'.repeat(numBackticks))) {
|
||||||
|
numBackticks++;
|
||||||
|
}
|
||||||
|
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
await fs.writeFile(filePath, modifiedContent, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedDiff;
|
||||||
|
}
|
||||||
|
|
||||||
// Tool handlers
|
// Tool handlers
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -233,6 +343,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
"Handles text content with proper encoding. Only works within allowed directories.",
|
"Handles text content with proper encoding. Only works within allowed directories.",
|
||||||
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput,
|
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "edit_file",
|
||||||
|
description:
|
||||||
|
"Make line-based edits to a text file. Each edit replaces exact line sequences " +
|
||||||
|
"with new content. Returns a git-style diff showing the changes made. " +
|
||||||
|
"Only works within allowed directories.",
|
||||||
|
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "create_directory",
|
name: "create_directory",
|
||||||
description:
|
description:
|
||||||
|
|
@ -346,6 +464,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "edit_file": {
|
||||||
|
const parsed = EditFileArgsSchema.safeParse(args);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for edit_file: ${parsed.error}`);
|
||||||
|
}
|
||||||
|
const validPath = await validatePath(parsed.data.path);
|
||||||
|
const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: result }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case "create_directory": {
|
case "create_directory": {
|
||||||
const parsed = CreateDirectoryArgsSchema.safeParse(args);
|
const parsed = CreateDirectoryArgsSchema.safeParse(args);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,13 @@
|
||||||
"watch": "tsc --watch"
|
"watch": "tsc --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "1.0.1",
|
"@modelcontextprotocol/sdk": "0.5.0",
|
||||||
|
"diff": "^5.1.0",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"zod-to-json-schema": "^3.23.5"
|
"zod-to-json-schema": "^3.23.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/diff": "^5.0.9",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue