1616 name = "Project Memory MCP" ,
1717 instructions = f"""
1818This MCP is for storing and retrieving project information to/from an English memory file.
19+
1920The memory file should store all information about the project in short and concise manner. It should be
2021good for humans and AI agents to catch up on the project status and progress quickly. Should contain descriptions,
2122ongoing tasks, tasks to do, references to files and other project resources, even URLs where to get more information.
2526Rules:
2627- This must be read by `get_project_memory` tool in the beginning of the first request of every conversation
2728 if the conversation is about a project and a full project path is provided.
28- - At the end of every answer the project memory must be updated using the `update_project_memory` tool.
29+ - At the end of every of your answers the project memory must be updated using the `update_project_memory`
30+ or `set_project_memory` tool if any relevant changes were made to the project or any useful information
31+ was discovered or discussed.
2932- The `set_project_memory` tool must be used to set the whole project memory if `update_project_memory`
3033 failed or there is no project memory yet.
3134- Never store any sensitive information in the memory file, e.g. personal information, company
32- information, passwords, access tokens, email addresses, etc.
35+ information, passwords, access tokens, email addresses, etc. !
3336- The memory file **must be in English**!
37+ - You can sometimes make it shorter, always remove any information that is no longer relevant.
38+ - Always remove any information that is no longer relevant from the memory file.
39+ - If the user talks about "memory" or "project memory", you should use the tools of this MCP.
3440"""
3541)
3642
@@ -74,11 +80,18 @@ def main():
7480
7581@mcp .tool ()
7682def get_project_memory (
77- project_path : str = Field (description = "The path to the project" )
83+ project_path : str = Field (description = "The full path to the project directory " )
7884) -> str :
7985 """
8086 Get the whole project memory for the given project path in Markdown format.
8187 This must be used in the beginning of the first request of every conversation.
88+
89+ The memory file contains vital information about the project such as descriptions,
90+ ongoing tasks, references to important files, and other project resources.
91+
92+ :return: The project memory content in Markdown format
93+ :raises FileNotFoundError: If the project path doesn't exist or MEMORY.md is missing
94+ :raises PermissionError: If the project path is not in allowed directories
8295 """
8396 pp = Path (project_path ).resolve ()
8497
@@ -95,13 +108,26 @@ def get_project_memory(
95108
96109@mcp .tool ()
97110def set_project_memory (
98- project_path : str = Field (description = "The path to the project" ),
99- project_info : str = Field (description = "The project information to set in Markdown format" )
111+ project_path : str = Field (description = "The full path to the project directory " ),
112+ project_info : str = Field (description = "Complete project information in Markdown format" )
100113):
101114 """
102115 Set the whole project memory for the given project path in Markdown format.
103- This should be used if the `update_project_memory` tool failed or there is no project memory yet.
104- The project memory file **must be in English**!
116+
117+ Use this tool when:
118+ - Creating a memory file for a new project
119+ - Completely replacing an existing memory file
120+ - When `update_project_memory` fails to apply patches
121+ - When extensive reorganization of the memory content is needed
122+
123+ Guidelines for content:
124+ - The project memory file **must be in English**!
125+ - Should be concise yet comprehensive
126+ - Remove outdated information
127+ - Include project overview, components, status, and important references
128+
129+ :raises FileNotFoundError: If the project path doesn't exist
130+ :raises PermissionError: If the project path is not in allowed directories
105131 """
106132 pp = Path (project_path ).resolve ()
107133 if not pp .exists () or not pp .is_dir ():
@@ -116,13 +142,23 @@ def set_project_memory(
116142def validate_block_integrity (patch_content ):
117143 """
118144 Validate the integrity of patch blocks before parsing.
119- Checks for balanced markers and correct sequence.
145+
146+ This function performs comprehensive validation of the patch format:
147+ 1. Checks for balanced markers (SEARCH, separator, REPLACE)
148+ 2. Verifies correct marker sequence
149+ 3. Detects nested markers inside blocks (which would cause errors)
150+
151+ All these checks happen before actual parsing to provide clear error
152+ messages and prevent corrupted patches from being applied.
153+
154+ :param patch_content: The raw patch content to validate
155+ :raises ValueError: With detailed message if any validation fails
120156 """
121157 # Check marker balance
122158 search_count = patch_content .count ("<<<<<<< SEARCH" )
123159 separator_count = patch_content .count ("=======" )
124160 replace_count = patch_content .count (">>>>>>> REPLACE" )
125-
161+
126162 if not (search_count == separator_count == replace_count ):
127163 raise ValueError (
128164 f"Malformed patch format: Unbalanced markers - "
@@ -135,7 +171,7 @@ def validate_block_integrity(patch_content):
135171 line = line .strip ()
136172 if line in ["<<<<<<< SEARCH" , "=======" , ">>>>>>> REPLACE" ]:
137173 markers .append (line )
138-
174+
139175 # Verify correct marker sequence (always SEARCH, SEPARATOR, REPLACE pattern)
140176 for i in range (0 , len (markers ), 3 ):
141177 if i + 2 < len (markers ):
@@ -144,7 +180,7 @@ def validate_block_integrity(patch_content):
144180 f"Malformed patch format: Incorrect marker sequence at position { i } : "
145181 f"Expected [SEARCH, SEPARATOR, REPLACE], got { markers [i :i + 3 ]} "
146182 )
147-
183+
148184 # Check for nested markers in each block
149185 sections = patch_content .split ("<<<<<<< SEARCH" )
150186 for i , section in enumerate (sections [1 :], 1 ): # Skip first empty section
@@ -155,13 +191,21 @@ def validate_block_integrity(patch_content):
155191def parse_search_replace_blocks (patch_content ):
156192 """
157193 Parse multiple search-replace blocks from the patch content.
158- Returns a list of tuples (search_text, replace_text).
194+
195+ This function first validates the block integrity, then extracts all
196+ search-replace pairs using either regex or line-by-line parsing as fallback.
197+ It also checks that search and replace texts don't contain markers themselves,
198+ which could lead to corrupted files.
199+
200+ :param patch_content: Raw patch content with SEARCH/REPLACE blocks
201+ :return: List of tuples (search_text, replace_text)
202+ :raises ValueError: If patch format is invalid or contains nested markers
159203 """
160204 # Define the markers
161205 search_marker = "<<<<<<< SEARCH"
162206 separator = "======="
163207 replace_marker = ">>>>>>> REPLACE"
164-
208+
165209 # First validate patch integrity
166210 validate_block_integrity (patch_content )
167211
@@ -200,13 +244,13 @@ def parse_search_replace_blocks(patch_content):
200244
201245 search_text = "\n " .join (lines [search_start :separator_idx ])
202246 replace_text = "\n " .join (lines [separator_idx + 1 :replace_end ])
203-
247+
204248 # Check for markers in the search or replace text
205249 if any (marker in search_text for marker in [search_marker , separator , replace_marker ]):
206250 raise ValueError (f"Block { len (blocks )+ 1 } : Search text contains patch markers" )
207251 if any (marker in replace_text for marker in [search_marker , separator , replace_marker ]):
208252 raise ValueError (f"Block { len (blocks )+ 1 } : Replace text contains patch markers" )
209-
253+
210254 blocks .append ((search_text , replace_text ))
211255
212256 i = replace_end + 1
@@ -230,14 +274,43 @@ def parse_search_replace_blocks(patch_content):
230274
231275@mcp .tool ()
232276def update_project_memory (
233- project_path : str = Field (description = "The path to the project" ),
234- patch_content : str = Field (description = "Unified diff/ patch to apply to the project memory " )
277+ project_path : str = Field (description = "The full path to the project directory " ),
278+ patch_content : str = Field (description = "Block-based patch content with SEARCH/REPLACE markers " )
235279):
236280 """
237- Update the project memory by applying a unified diff/patch to the memory file.
238-
239- :param project_path: The path to the project directory.
240- :param patch_content: Unified diff/patch to apply.
281+ Update the project memory by applying a block-based patch to the memory file.
282+
283+ Required block format:
284+ ```
285+ <<<<<<< SEARCH
286+ Text to find in the memory file
287+ =======
288+ Text to replace it with
289+ >>>>>>> REPLACE
290+ ```
291+
292+ You can include multiple search-replace blocks in a single request:
293+ ```
294+ <<<<<<< SEARCH
295+ First text to find
296+ =======
297+ First replacement
298+ >>>>>>> REPLACE
299+ <<<<<<< SEARCH
300+ Second text to find
301+ =======
302+ Second replacement
303+ >>>>>>> REPLACE
304+ ```
305+
306+ This tool verifies that each search text appears exactly once in the file to ensure
307+ the correct section is modified. If a search text appears multiple times or isn't
308+ found, it will report an error.
309+
310+ :return: Success message with number of blocks applied
311+ :raises FileNotFoundError: If the project path or memory file doesn't exist
312+ :raises ValueError: If patch format is invalid or search text isn't unique
313+ :raises RuntimeError: If patch application fails for any reason
241314 """
242315 project_dir = Path (project_path ).resolve ()
243316 if not project_dir .is_dir ():
@@ -278,11 +351,11 @@ def update_project_memory(
278351 elif count > 1 :
279352 # Multiple matches - too ambiguous
280353 raise ValueError (f"Block { i + 1 } : The search text appears { count } times in the file. "
281- "Please provide more context to identify the specific occurrence." )
354+ "Please provide more context to identify the specific occurrence." )
282355 else :
283356 # No match found
284357 raise ValueError (f"Block { i + 1 } : Could not find the search text in the file. "
285- "Please ensure the search text exactly matches the content in the file." )
358+ "Please ensure the search text exactly matches the content in the file." )
286359
287360 # Write the final content back to the file
288361 with open (memory_file , 'w' , encoding = 'utf-8' ) as f :
@@ -292,11 +365,11 @@ def update_project_memory(
292365 except Exception as block_error :
293366 # If block format parsing fails, log the error and try traditional patch format
294367 eprint (f"Block format parsing failed: { str (block_error )} " )
295-
368+
296369 # If you still want to support traditional patches with whatthepatch or similar, add that code here
297370 # For now, we'll just raise the error from block parsing
298371 raise block_error
299-
372+
300373 except Exception as e :
301374 # If anything goes wrong, provide detailed error
302375 raise RuntimeError (f"Failed to apply patch: { str (e )} " )
0 commit comments