Skip to content

Commit 04d1a98

Browse files
author
Andy Klier
committed
moving to state mgmt
1 parent b41cc36 commit 04d1a98

File tree

2 files changed

+112
-42
lines changed

2 files changed

+112
-42
lines changed

src/filesystem/index.ts

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { diffLines, createTwoFilesPatch } from 'diff';
1616
import { minimatch } from 'minimatch';
1717
import { simpleGit, SimpleGit } from 'simple-git';
1818

19+
// Import state utilities
20+
import { getState, saveState, hasValidatedInPrompt, markValidatedInPrompt, resetValidationState } from './state-utils.js';
21+
1922
// Command line argument parsing
2023
const args = process.argv.slice(2);
2124

@@ -135,14 +138,15 @@ async function isGitClean(filePath: string): Promise<{isRepo: boolean, isClean:
135138
// Check if the Git status allows modification
136139
// With the One-Check-Per-Prompt approach, validation is only performed
137140
// on the first file operation in each prompt and skipped for subsequent operations
138-
async function validateGitStatus(filePath: string): Promise<void> {
141+
async function validateGitStatus(filePath: string, promptId?: string): Promise<void> {
139142
if (!gitConfig.requireCleanBranch) {
140143
return; // Git validation is disabled
141144
}
142145

143146
// Skip if we've already checked in this prompt
144-
if (gitConfig.checkedThisPrompt) {
145-
console.log(`Skipping validation for ${filePath} - already checked`);
147+
const hasValidated = await hasValidatedInPrompt(promptId);
148+
if (hasValidated) {
149+
console.log('Skipping validation for ' + filePath + ' - already validated in this prompt');
146150
return;
147151
}
148152

@@ -151,36 +155,27 @@ async function validateGitStatus(filePath: string): Promise<void> {
151155
// When requireCleanBranch is set, we require the file to be in a Git repository
152156
if (!isRepo) {
153157
throw new Error(
154-
`The file ${filePath} is not in a Git repository. ` +
155-
`This server is configured to require files to be in Git repositories with clean branches.`
156-
);
158+
"The file " + filePath + " is not in a Git repository. " +
159+
"This server is configured to require files to be in Git repositories with clean branches."
160+
);
157161
}
158162

159163
// And we require the repository to be clean
160164
if (!isClean) {
161-
throw new Error(
162-
`Git repository at ${repoPath} has uncommitted changes. ` +
163-
`This server is configured to require a clean branch before allowing changes.`
164-
);
165+
throw new Error(
166+
"Git repository at " + repoPath + " has uncommitted changes. " +
167+
"This server is configured to require a clean branch before allowing changes."
168+
);
165169
}
166170

167171
// Mark that we've checked in this prompt
168-
gitConfig.checkedThisPrompt = true;
169-
console.log(`Validation passed for ${filePath} - marked as checked`);
170-
}
171-
172-
// Reset the validation state
173-
// This function is called after each tool request to reset the validation state
174-
// so that the next prompt will perform a fresh validation
175-
function resetValidationState(): void {
176-
if (gitConfig.checkedThisPrompt) {
177-
gitConfig.checkedThisPrompt = false;
178-
console.log("State reset for next prompt");
179-
}
172+
await markValidatedInPrompt(promptId);
173+
console.log('Validation passed for ' + filePath + ' - marked as checked');
180174
}
181175

176+
// Git validation utilities
182177
// Security utilities
183-
async function validatePath(requestedPath: string, skipGitCheck: boolean = false): Promise<string> {
178+
async function validatePath(requestedPath: string, skipGitCheck: boolean = false, promptId?: string): Promise<string> {
184179
const expandedPath = expandHome(requestedPath);
185180
const absolute = path.isAbsolute(expandedPath)
186181
? path.resolve(expandedPath)
@@ -205,7 +200,7 @@ async function validatePath(requestedPath: string, skipGitCheck: boolean = false
205200

206201
// Perform Git validation if required
207202
if (!skipGitCheck && gitConfig.requireCleanBranch) {
208-
await validateGitStatus(realPath);
203+
await validateGitStatus(realPath, promptId);
209204
}
210205

211206
return realPath;
@@ -222,7 +217,7 @@ async function validatePath(requestedPath: string, skipGitCheck: boolean = false
222217

223218
// Perform Git validation on the parent directory for new files
224219
if (!skipGitCheck && gitConfig.requireCleanBranch) {
225-
await validateGitStatus(parentDir);
220+
await validateGitStatus(parentDir, promptId);
226221
}
227222

228223
return absolute;
@@ -334,7 +329,8 @@ async function getFileStats(filePath: string): Promise<FileInfo> {
334329
async function searchFiles(
335330
rootPath: string,
336331
pattern: string,
337-
excludePatterns: string[] = []
332+
excludePatterns: string[] = [],
333+
promptId?: string
338334
): Promise<string[]> {
339335
const results: string[] = [];
340336

@@ -346,7 +342,7 @@ async function searchFiles(
346342

347343
try {
348344
// Validate each path before processing (skip Git check for search)
349-
await validatePath(fullPath, true);
345+
await validatePath(fullPath, true, promptId);
350346

351347
// Check if path matches any exclude pattern
352348
const relativePath = path.relative(rootPath, fullPath);
@@ -596,6 +592,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
596592
server.setRequestHandler(CallToolRequestSchema, async (request) => {
597593
try {
598594
const { name, arguments: args } = request.params;
595+
596+
// Generate a unique prompt ID for this request
597+
const promptId = 'prompt-' + Date.now() + '-' + Math.floor(Math.random() * 10000);
598+
console.log('Processing request ' + name + ' with promptId: ' + promptId);
599599

600600
// Get the response from the appropriate tool handler
601601
let response;
@@ -606,7 +606,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
606606
if (!parsed.success) {
607607
throw new Error(`Invalid arguments for read_file: ${parsed.error}`);
608608
}
609-
const validPath = await validatePath(parsed.data.path, true); // Skip Git check for read-only operation
609+
const validPath = await validatePath(parsed.data.path, true, promptId); // Skip Git check for read-only operation
610610
const content = await fs.readFile(validPath, "utf-8");
611611
response = {
612612
content: [{ type: "text", text: content }],
@@ -622,7 +622,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
622622
const results = await Promise.all(
623623
parsed.data.paths.map(async (filePath: string) => {
624624
try {
625-
const validPath = await validatePath(filePath, true); // Skip Git check for read-only operation
625+
const validPath = await validatePath(filePath, true, promptId); // Skip Git check for read-only operation
626626
const content = await fs.readFile(validPath, "utf-8");
627627
return `${filePath}:\n${content}\n`;
628628
} catch (error) {
@@ -642,7 +642,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
642642
if (!parsed.success) {
643643
throw new Error(`Invalid arguments for write_file: ${parsed.error}`);
644644
}
645-
const validPath = await validatePath(parsed.data.path); // Git check is now performed in validatePath
645+
const validPath = await validatePath(parsed.data.path, false, promptId); // Git check is now performed in validatePath
646646

647647
await fs.writeFile(validPath, parsed.data.content, "utf-8");
648648
response = {
@@ -658,7 +658,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
658658
}
659659

660660
// If this is a dry run, skip Git check
661-
const validPath = await validatePath(parsed.data.path, parsed.data.dryRun);
661+
const validPath = await validatePath(parsed.data.path, parsed.data.dryRun, promptId);
662662

663663
const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun);
664664
response = {
@@ -672,7 +672,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
672672
if (!parsed.success) {
673673
throw new Error(`Invalid arguments for create_directory: ${parsed.error}`);
674674
}
675-
const validPath = await validatePath(parsed.data.path); // Git check is now performed in validatePath
675+
const validPath = await validatePath(parsed.data.path, false, promptId); // Git check is now performed in validatePath
676676

677677
await fs.mkdir(validPath, { recursive: true });
678678
response = {
@@ -686,7 +686,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
686686
if (!parsed.success) {
687687
throw new Error(`Invalid arguments for list_directory: ${parsed.error}`);
688688
}
689-
const validPath = await validatePath(parsed.data.path, true); // Skip Git check for read-only operation
689+
const validPath = await validatePath(parsed.data.path, true, promptId); // Skip Git check for read-only operation
690690
const entries = await fs.readdir(validPath, { withFileTypes: true });
691691
const formatted = entries
692692
.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
@@ -710,7 +710,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
710710
}
711711

712712
async function buildTree(currentPath: string): Promise<TreeEntry[]> {
713-
const validPath = await validatePath(currentPath, true); // Skip Git check for read-only operation
713+
const validPath = await validatePath(currentPath, true, promptId); // Skip Git check for read-only operation
714714
const entries = await fs.readdir(validPath, {withFileTypes: true});
715715
const result: TreeEntry[] = [];
716716

@@ -746,8 +746,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
746746
if (!parsed.success) {
747747
throw new Error(`Invalid arguments for move_file: ${parsed.error}`);
748748
}
749-
const validSourcePath = await validatePath(parsed.data.source); // Git check is now performed in validatePath
750-
const validDestPath = await validatePath(parsed.data.destination); // Git check is now performed in validatePath
749+
const validSourcePath = await validatePath(parsed.data.source, false, promptId); // Git check is now performed in validatePath
750+
const validDestPath = await validatePath(parsed.data.destination, false, promptId); // Git check is now performed in validatePath
751751

752752
await fs.rename(validSourcePath, validDestPath);
753753
response = {
@@ -761,8 +761,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
761761
if (!parsed.success) {
762762
throw new Error(`Invalid arguments for search_files: ${parsed.error}`);
763763
}
764-
const validPath = await validatePath(parsed.data.path, true); // Skip Git check for read-only operation
765-
const results = await searchFiles(validPath, parsed.data.pattern, parsed.data.excludePatterns);
764+
const validPath = await validatePath(parsed.data.path, true, promptId); // Skip Git check for read-only operation
765+
const results = await searchFiles(validPath, parsed.data.pattern, parsed.data.excludePatterns, promptId);
766766
response = {
767767
content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }],
768768
};
@@ -774,7 +774,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
774774
if (!parsed.success) {
775775
throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`);
776776
}
777-
const validPath = await validatePath(parsed.data.path, true); // Skip Git check for read-only operation
777+
const validPath = await validatePath(parsed.data.path, true, promptId); // Skip Git check for read-only operation
778778
const info = await getFileStats(validPath);
779779
response = {
780780
content: [{ type: "text", text: Object.entries(info)
@@ -799,7 +799,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
799799
if (!parsed.success) {
800800
throw new Error(`Invalid arguments for git_status: ${parsed.error}`);
801801
}
802-
const validPath = await validatePath(parsed.data.path, true); // Skip Git check for read-only operation
802+
const validPath = await validatePath(parsed.data.path, true, promptId); // Skip Git check for read-only operation
803803

804804
// Get Git status information
805805
const gitStatus = await isGitClean(validPath);
@@ -842,12 +842,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
842842
}
843843

844844
// Reset validation state after successful response
845-
resetValidationState();
845+
await resetValidationState();
846846

847847
return response;
848848
} catch (error) {
849849
// Reset validation state even on error
850-
resetValidationState();
850+
await resetValidationState();
851851

852852
const errorMessage = error instanceof Error ? error.message : String(error);
853853
return {

src/filesystem/state-utils.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import fs from "fs/promises";
2+
3+
// Define the state interface
4+
interface PromptState {
5+
checkedThisPrompt: boolean;
6+
promptId: string | null;
7+
lastCheckTime: number;
8+
}
9+
10+
// Define a location for the state file
11+
const STATE_FILE_PATH = '/tmp/mcp-filesystem-state.json';
12+
13+
// State management functions
14+
export async function getState(): Promise<PromptState> {
15+
try {
16+
const stateData = await fs.readFile(STATE_FILE_PATH, 'utf-8');
17+
return JSON.parse(stateData);
18+
} catch (error) {
19+
// If file doesn't exist or has invalid content, return default state
20+
return { checkedThisPrompt: false, promptId: null, lastCheckTime: 0 };
21+
}
22+
}
23+
24+
export async function saveState(state: PromptState): Promise<void> {
25+
await fs.writeFile(STATE_FILE_PATH, JSON.stringify(state, null, 2), 'utf-8');
26+
}
27+
28+
// Reset the validation state
29+
export async function resetValidationState(): Promise<void> {
30+
const state = await getState();
31+
if (state.checkedThisPrompt) {
32+
state.checkedThisPrompt = false;
33+
state.promptId = null;
34+
await saveState(state);
35+
console.log("State reset for next prompt");
36+
}
37+
}
38+
39+
// Check if we've already validated in this prompt
40+
export async function hasValidatedInPrompt(promptId?: string): Promise<boolean> {
41+
const state = await getState();
42+
43+
// Generate a new promptId if one wasn't provided
44+
promptId = promptId || 'prompt-' + Date.now();
45+
46+
// If this is a new prompt or too much time has passed, reset state
47+
const now = Date.now();
48+
const MAX_PROMPT_AGE = 60 * 1000; // 60 seconds
49+
50+
if (state.promptId !== promptId || (now - state.lastCheckTime) > MAX_PROMPT_AGE) {
51+
state.checkedThisPrompt = false;
52+
state.promptId = promptId;
53+
}
54+
55+
// Update last check time
56+
state.lastCheckTime = now;
57+
await saveState(state);
58+
59+
return state.checkedThisPrompt;
60+
}
61+
62+
// Mark that we've validated in this prompt
63+
export async function markValidatedInPrompt(promptId?: string): Promise<void> {
64+
const state = await getState();
65+
state.checkedThisPrompt = true;
66+
state.promptId = promptId || 'prompt-' + Date.now();
67+
state.lastCheckTime = Date.now();
68+
await saveState(state);
69+
console.log('Validation marked as complete for prompt: ' + state.promptId);
70+
}

0 commit comments

Comments
 (0)