Skip to content

Commit 43e3dee

Browse files
committed
Update version to 0.1.0 in pyproject.toml and refine server.py documentation. Enhance project memory tools with clearer guidelines and error handling for patch updates.
1 parent fd9ebd1 commit 43e3dee

File tree

2 files changed

+100
-27
lines changed

2 files changed

+100
-27
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[project]
22
name = "project-mem-mcp"
3-
version = "0.2.0"
3+
version = "0.1.0"
44
description = "An MCP Server to store and retreive project information from memory file"
55
authors = [{ name = "PYNESYS LLC" }]
66
readme = "README.md"
77
requires-python = ">=3.11"
88
license = { text = "MIT" }
99

10-
dependencies = ["fastmcp>=2.2.0, <3.0.0", "unidiff>=0.7.0, <0.8.0"]
10+
dependencies = ["fastmcp>=2.2.0, <3.0.0"]
1111

1212
[project.scripts]
1313
project-mem-mcp = "project_mem_mcp.server:main"

src/project_mem_mcp/server.py

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
name="Project Memory MCP",
1717
instructions=f"""
1818
This MCP is for storing and retrieving project information to/from an English memory file.
19+
1920
The memory file should store all information about the project in short and concise manner. It should be
2021
good for humans and AI agents to catch up on the project status and progress quickly. Should contain descriptions,
2122
ongoing tasks, tasks to do, references to files and other project resources, even URLs where to get more information.
@@ -25,12 +26,17 @@
2526
Rules:
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()
7682
def 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()
97110
def 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(
116142
def 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):
155191
def 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()
232276
def 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

Comments
 (0)