• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nogoo9 / mcp-server-cloud-fs / 26046589771

18 May 2026 04:32PM UTC coverage: 68.233% (+1.4%) from 66.832%
26046589771

Pull #27

github

web-flow
Merge 0078abe56 into 37d582198
Pull Request #27: feat: v0.7.0 — AI-native tools, DLP, ETags, scope filtering, patch_file

447 of 563 new or added lines in 7 files covered. (79.4%)

3398 of 4980 relevant lines covered (68.23%)

23.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.0
/src/tools/write.ts
1
// src/tools/write.ts
2

3
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
import { z } from "zod";
24✔
5
import { resolveToolPath } from "../path-utils.js";
51✔
6
import type { ParsedRoot } from "../providers/interface.js";
7
import type { VirtualFS } from "../vfs.js";
8

9
type Ctx = {
10
        vfs: VirtualFS;
11
        roots: ParsedRoot[];
12
};
13
type ToolResult = {
14
        content: [{ type: "text"; text: string }];
15
        isError?: boolean;
16
};
17

18
export async function handleWriteFile(
6✔
19
        args: { path: string; content: string },
6✔
20
        ctx: Ctx,
4✔
21
): Promise<ToolResult> {
3✔
22
        try {
9✔
23
                const { root, key } = resolveToolPath(ctx.roots, args.path);
64✔
24
                const buffer = Buffer.from(args.content, "utf8");
53✔
25
                await ctx.vfs.put(root, key, buffer);
41✔
26
                return {
14✔
27
                        content: [{ type: "text", text: `Successfully wrote to ${args.path}` }],
75✔
28
                };
2✔
29
        } catch (err) {
17✔
30
                return {
14✔
31
                        isError: true,
20✔
32
                        content: [{ type: "text", text: (err as Error).message }],
50✔
33
                };
2✔
34
        }
35
}
36

37
export async function handleEditFile(
6✔
38
        args: {
6✔
39
                path: string;
40
                edits: Array<{ oldText: string; newText: string }>;
41
                dryRun?: boolean | undefined;
42
                expected_etag?: string | undefined;
43
        },
44
        ctx: Ctx,
4✔
45
): Promise<ToolResult> {
3✔
46
        try {
9✔
47
                const { root, key } = resolveToolPath(ctx.roots, args.path);
64✔
48

49
                // Read via VFS (cache-first, then provider)
50
                const buffer = await ctx.vfs.get(root, key);
48✔
51
                const original = buffer.toString("utf8");
45✔
52
                let content = original;
27✔
53

54
                // Optimistic concurrency: check etag before editing
55
                if (args.expected_etag) {
30✔
56
                        const stat = await ctx.vfs.stat(root, key);
49✔
57
                        if (stat.etag && stat.etag !== args.expected_etag) {
59✔
58
                                return {
18✔
59
                                        isError: true,
24✔
60
                                        content: [
22✔
61
                                                {
15✔
62
                                                        type: "text",
27✔
63
                                                        text:
6✔
64
                                                                `Conflict: file has been modified since last read. ` +
68✔
65
                                                                `Expected etag: ${args.expected_etag}, current etag: ${stat.etag}. ` +
89✔
66
                                                                `Re-read the file and retry.`,
67
                                                },
11✔
68
                                        ],
9✔
69
                                };
1✔
70
                        }
4✔
71
                }
4✔
72

73
                // Apply edits sequentially
74
                for (const { oldText, newText } of args.edits) {
53✔
75
                        if (!content.includes(oldText)) {
40✔
76
                                throw new Error(
16✔
77
                                        `edit_file: oldText not found in ${args.path}: ${JSON.stringify(oldText.slice(0, 40))}`,
87✔
78
                                );
1✔
79
                        }
6✔
80
                        content = content.replace(oldText, newText);
48✔
81
                }
4✔
82

83
                if (args.dryRun) {
23✔
84
                        return {
16✔
85
                                content: [
20✔
86
                                        { type: "text", text: formatDiff(args.path, args.edits, original) },
75✔
87
                                ],
7✔
88
                        };
1✔
89
                }
4✔
90

91
                const newBuffer = Buffer.from(content, "utf8");
51✔
92
                await ctx.vfs.put(root, key, newBuffer);
44✔
93
                const newStat = await ctx.vfs.stat(root, key);
50✔
94
                const etagInfo = newStat.etag ? ` (etag: ${newStat.etag})` : "";
61✔
95
                return {
14✔
96
                        content: [
18✔
97
                                { type: "text", text: `Successfully edited ${args.path}${etagInfo}` },
75✔
98
                        ],
5✔
99
                };
2✔
100
        } catch (err) {
17✔
101
                return {
14✔
102
                        isError: true,
20✔
103
                        content: [{ type: "text", text: (err as Error).message }],
50✔
104
                };
1✔
105
        }
106
}
107

108
function formatDiff(
1✔
109
        path: string,
6✔
110
        edits: Array<{ oldText: string; newText: string }>,
7✔
111
        original: string,
10✔
112
): string {
3✔
113
        let out = `Dry run — edits to ${path}:\n\n`;
47✔
114
        for (let i = 0; i < edits.length; i++) {
42✔
115
                const { oldText, newText } = edits[i]!;
42✔
116
                const found = original.includes(oldText);
45✔
117
                out += `Edit ${i + 1}${found ? "" : " (NOT FOUND — would error)"}:\n`;
39✔
118
                for (const line of oldText.split("\n")) out += `- ${line}\n`;
64✔
119
                for (const line of newText.split("\n")) out += `+ ${line}\n`;
64✔
120
                out += "\n";
12✔
121
        }
2✔
122
        return out.trimEnd();
21✔
123
}
124

125
export function registerWriteTools(server: McpServer, ctx: Ctx): void {
×
126
        server.registerTool(
×
127
                "write_file",
×
128
                {
×
129
                        description: "Write content to a file, creating it if it does not exist.",
×
130
                        inputSchema: z.object({ path: z.string(), content: z.string() }),
×
131
                },
×
132
                async (args) => handleWriteFile(args, ctx),
×
133
        );
×
134

×
135
        server.registerTool(
×
136
                "edit_file",
×
137
                {
×
138
                        description:
×
NEW
139
                                "Apply text edits to a file. Each edit replaces oldText with newText. Use dryRun to preview. " +
×
NEW
140
                                "Optionally pass expected_etag for optimistic concurrency control.",
×
141
                        inputSchema: z.object({
×
142
                                path: z.string(),
×
143
                                edits: z.array(z.object({ oldText: z.string(), newText: z.string() })),
×
144
                                dryRun: z.boolean().optional(),
×
NEW
145
                                expected_etag: z
×
NEW
146
                                        .string()
×
NEW
147
                                        .optional()
×
NEW
148
                                        .describe(
×
NEW
149
                                                "If provided, the edit is rejected when the current file etag does not match (conflict detection)",
×
NEW
150
                                        ),
×
151
                        }),
×
152
                },
×
153
                async (args) => handleEditFile(args, ctx),
×
154
        );
155
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc