project-mem-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 PYNESYS LLC.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: project-mem-mcp
3
+ Version: 0.1.0
4
+ Summary: An MCP Server to store and retreive project information from memory file
5
+ Author: PYNESYS LLC
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: fastmcp<3.0.0,>=2.2.0
11
+ Dynamic: license-file
12
+
13
+ # Project Memory MCP
14
+
15
+ An MCP Server to store and retrieve project information from memory file. This allows AI agents (like Claude) to maintain persistent memory about projects between conversations.
16
+
17
+ ## Overview
18
+
19
+ Project Memory MCP provides a simple way to:
20
+ - Store project information in Markdown format
21
+ - Retrieve project information at the beginning of conversations
22
+ - Update project information using patches
23
+
24
+ The memory is stored in a `MEMORY.md` file in each project directory.
25
+
26
+ ## Installation
27
+
28
+ ### Using uvx
29
+
30
+ This method uses `uvx` (from the `uv` Python package manager) to run the server without permanent installation:
31
+
32
+ #### Prerequisites
33
+
34
+ Install `uvx` from [uv](https://docs.astral.sh/uv/installation/) if you don't have it already.
35
+
36
+ #### Set up MCP Client (Claude Desktop, Cursor, etc.)
37
+
38
+ Merge the following config with your existing config file (e.g. `claude_desktop_config.json`):
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "project-memory": {
44
+ "command": "uvx",
45
+ "args": [
46
+ "project-mem-mcp",
47
+ "--allowed-dir", "/Users/your-username/projects",
48
+ "--allowed-dir", "/Users/your-username/Documents/code"
49
+ ]
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ > **Note:** Replace `/Users/your-username` with the actual path to your own projects and code directories.
56
+
57
+ ### Install from Source
58
+
59
+ #### Prerequisites
60
+
61
+ - Python 3.11 or higher
62
+ - Pip package manager
63
+
64
+ #### Clone the repository
65
+
66
+ ```bash
67
+ git clone https://github.com/your-username/project-mem-mcp.git
68
+ python -m venv venv
69
+ source venv/bin/activate
70
+ pip install -e .
71
+ ```
72
+
73
+ #### Set up MCP Client (Claude Desktop, Cursor, etc.)
74
+
75
+ Merge the following config with your existing config file (e.g. `claude_desktop_config.json`):
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "project-memory": {
81
+ "command": "path/to/your/venv/bin/project-mem-mcp",
82
+ "args": [
83
+ "--allowed-dir", "/Users/your-username/projects",
84
+ "--allowed-dir", "/Users/your-username/Documents/code"
85
+ ]
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ > **Note:** Replace `/Users/your-username` with the actual path to your own projects and code directories.
92
+
93
+ ## Arguments
94
+
95
+ The `--allowed-dir` argument is used to specify the directories that the server has access to. You can use it multiple times to allow access to multiple directories. All directories inside the allowed directories are also allowed.
96
+ It is optional. If not provided, the server will only have access to the home directory of the user running the server.
97
+
98
+
99
+ ## Usage
100
+
101
+ The MCP server is started by the client (e.g., Claude Desktop) based on the configuration you provide. You don't need to start the server manually.
102
+
103
+ ### Tools
104
+
105
+ Project Memory MCP provides three tools:
106
+
107
+ #### get_project_memory
108
+
109
+ Retrieves the entire project memory in Markdown format. Should be used at the beginning of every conversation about a project.
110
+
111
+ ```python
112
+ get_project_memory(project_path: str) -> str
113
+ ```
114
+ - **project_path**: Full path to the project directory.
115
+ - Returns the content of the MEMORY.md file as a string.
116
+ - Raises `FileNotFoundError` if the project or memory file does not exist.
117
+ - Raises `PermissionError` if the project path is not in allowed directories.
118
+
119
+ #### set_project_memory
120
+
121
+ Sets (overwrites) the entire project memory. Use this when creating a new memory file, replacing the whole memory, or if `update_project_memory` fails.
122
+
123
+ ```python
124
+ set_project_memory(project_path: str, project_info: str)
125
+ ```
126
+ - **project_path**: Full path to the project directory.
127
+ - **project_info**: Complete project information in Markdown format.
128
+ - Overwrites the MEMORY.md file with the provided content.
129
+ - Raises `FileNotFoundError` if the project path does not exist.
130
+ - Raises `PermissionError` if the project path is not in allowed directories.
131
+
132
+ #### update_project_memory
133
+
134
+ Updates the project memory by applying one or more block-based patches to the memory file. This is more efficient for small changes.
135
+
136
+ ```python
137
+ update_project_memory(project_path: str, patch_content: str)
138
+ ```
139
+ - **project_path**: Full path to the project directory.
140
+ - **patch_content**: Block-based patch content using SEARCH/REPLACE markers (see below).
141
+ - Each patch block must have the following format:
142
+
143
+ ```
144
+ <<<<<<< SEARCH
145
+ Text to find in the memory file
146
+ =======
147
+ Text to replace it with
148
+ >>>>>>> REPLACE
149
+ ```
150
+ Multiple blocks can be included in a single request.
151
+ - Each search text must appear **exactly once** in the file, otherwise an error is raised.
152
+ - Raises `FileNotFoundError` if the project or memory file does not exist.
153
+ - Raises `ValueError` if the patch format is invalid or the search text is not unique.
154
+ - Raises `RuntimeError` if patch application fails for any reason.
155
+
156
+ ### Example Workflow
157
+
158
+ 1. Begin a conversation with LLM about a project
159
+ 2. LLM uses `get_project_memory` to retrieve project information
160
+ 3. Throughout the conversation, LLM uses `update_project_memory` to persist new information
161
+ 4. If the update fails, LLM can use `set_project_memory` instead
162
+
163
+ #### Claude Desktop
164
+
165
+ if you use Claude Desktop, it is best to use the project feature.
166
+
167
+ Edit the project instructions:
168
+ - Add a line like this: "The path of the project is <project_path>"
169
+ - If it does not always use the memory, you can add a line like this: "Always use the project memory, it is not optional"
170
+
171
+ ## Security Considerations
172
+
173
+ - Memory files should never contain sensitive information
174
+ - Project paths are validated against allowed directories
175
+ - All file operations are restricted to allowed directories
176
+
177
+ ## Dependencies
178
+
179
+ - fastmcp (>=2.2.0, <3.0.0)
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,171 @@
1
+ # Project Memory MCP
2
+
3
+ An MCP Server to store and retrieve project information from memory file. This allows AI agents (like Claude) to maintain persistent memory about projects between conversations.
4
+
5
+ ## Overview
6
+
7
+ Project Memory MCP provides a simple way to:
8
+ - Store project information in Markdown format
9
+ - Retrieve project information at the beginning of conversations
10
+ - Update project information using patches
11
+
12
+ The memory is stored in a `MEMORY.md` file in each project directory.
13
+
14
+ ## Installation
15
+
16
+ ### Using uvx
17
+
18
+ This method uses `uvx` (from the `uv` Python package manager) to run the server without permanent installation:
19
+
20
+ #### Prerequisites
21
+
22
+ Install `uvx` from [uv](https://docs.astral.sh/uv/installation/) if you don't have it already.
23
+
24
+ #### Set up MCP Client (Claude Desktop, Cursor, etc.)
25
+
26
+ Merge the following config with your existing config file (e.g. `claude_desktop_config.json`):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "project-memory": {
32
+ "command": "uvx",
33
+ "args": [
34
+ "project-mem-mcp",
35
+ "--allowed-dir", "/Users/your-username/projects",
36
+ "--allowed-dir", "/Users/your-username/Documents/code"
37
+ ]
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ > **Note:** Replace `/Users/your-username` with the actual path to your own projects and code directories.
44
+
45
+ ### Install from Source
46
+
47
+ #### Prerequisites
48
+
49
+ - Python 3.11 or higher
50
+ - Pip package manager
51
+
52
+ #### Clone the repository
53
+
54
+ ```bash
55
+ git clone https://github.com/your-username/project-mem-mcp.git
56
+ python -m venv venv
57
+ source venv/bin/activate
58
+ pip install -e .
59
+ ```
60
+
61
+ #### Set up MCP Client (Claude Desktop, Cursor, etc.)
62
+
63
+ Merge the following config with your existing config file (e.g. `claude_desktop_config.json`):
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "project-memory": {
69
+ "command": "path/to/your/venv/bin/project-mem-mcp",
70
+ "args": [
71
+ "--allowed-dir", "/Users/your-username/projects",
72
+ "--allowed-dir", "/Users/your-username/Documents/code"
73
+ ]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ > **Note:** Replace `/Users/your-username` with the actual path to your own projects and code directories.
80
+
81
+ ## Arguments
82
+
83
+ The `--allowed-dir` argument is used to specify the directories that the server has access to. You can use it multiple times to allow access to multiple directories. All directories inside the allowed directories are also allowed.
84
+ It is optional. If not provided, the server will only have access to the home directory of the user running the server.
85
+
86
+
87
+ ## Usage
88
+
89
+ The MCP server is started by the client (e.g., Claude Desktop) based on the configuration you provide. You don't need to start the server manually.
90
+
91
+ ### Tools
92
+
93
+ Project Memory MCP provides three tools:
94
+
95
+ #### get_project_memory
96
+
97
+ Retrieves the entire project memory in Markdown format. Should be used at the beginning of every conversation about a project.
98
+
99
+ ```python
100
+ get_project_memory(project_path: str) -> str
101
+ ```
102
+ - **project_path**: Full path to the project directory.
103
+ - Returns the content of the MEMORY.md file as a string.
104
+ - Raises `FileNotFoundError` if the project or memory file does not exist.
105
+ - Raises `PermissionError` if the project path is not in allowed directories.
106
+
107
+ #### set_project_memory
108
+
109
+ Sets (overwrites) the entire project memory. Use this when creating a new memory file, replacing the whole memory, or if `update_project_memory` fails.
110
+
111
+ ```python
112
+ set_project_memory(project_path: str, project_info: str)
113
+ ```
114
+ - **project_path**: Full path to the project directory.
115
+ - **project_info**: Complete project information in Markdown format.
116
+ - Overwrites the MEMORY.md file with the provided content.
117
+ - Raises `FileNotFoundError` if the project path does not exist.
118
+ - Raises `PermissionError` if the project path is not in allowed directories.
119
+
120
+ #### update_project_memory
121
+
122
+ Updates the project memory by applying one or more block-based patches to the memory file. This is more efficient for small changes.
123
+
124
+ ```python
125
+ update_project_memory(project_path: str, patch_content: str)
126
+ ```
127
+ - **project_path**: Full path to the project directory.
128
+ - **patch_content**: Block-based patch content using SEARCH/REPLACE markers (see below).
129
+ - Each patch block must have the following format:
130
+
131
+ ```
132
+ <<<<<<< SEARCH
133
+ Text to find in the memory file
134
+ =======
135
+ Text to replace it with
136
+ >>>>>>> REPLACE
137
+ ```
138
+ Multiple blocks can be included in a single request.
139
+ - Each search text must appear **exactly once** in the file, otherwise an error is raised.
140
+ - Raises `FileNotFoundError` if the project or memory file does not exist.
141
+ - Raises `ValueError` if the patch format is invalid or the search text is not unique.
142
+ - Raises `RuntimeError` if patch application fails for any reason.
143
+
144
+ ### Example Workflow
145
+
146
+ 1. Begin a conversation with LLM about a project
147
+ 2. LLM uses `get_project_memory` to retrieve project information
148
+ 3. Throughout the conversation, LLM uses `update_project_memory` to persist new information
149
+ 4. If the update fails, LLM can use `set_project_memory` instead
150
+
151
+ #### Claude Desktop
152
+
153
+ if you use Claude Desktop, it is best to use the project feature.
154
+
155
+ Edit the project instructions:
156
+ - Add a line like this: "The path of the project is <project_path>"
157
+ - If it does not always use the memory, you can add a line like this: "Always use the project memory, it is not optional"
158
+
159
+ ## Security Considerations
160
+
161
+ - Memory files should never contain sensitive information
162
+ - Project paths are validated against allowed directories
163
+ - All file operations are restricted to allowed directories
164
+
165
+ ## Dependencies
166
+
167
+ - fastmcp (>=2.2.0, <3.0.0)
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "project-mem-mcp"
3
+ version = "0.1.0"
4
+ description = "An MCP Server to store and retreive project information from memory file"
5
+ authors = [{ name = "PYNESYS LLC" }]
6
+ readme = "README.md"
7
+ requires-python = ">=3.11"
8
+ license = { text = "MIT" }
9
+
10
+ dependencies = ["fastmcp>=2.2.0, <3.0.0"]
11
+
12
+ [project.scripts]
13
+ project-mem-mcp = "project_mem_mcp.server:main"
14
+
15
+ [build-system]
16
+ requires = ["setuptools>=68.0", "wheel"]
17
+ build-backend = "setuptools.build_meta"
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from fastmcp import FastMCP
7
+ from pydantic.fields import Field
8
+ import re
9
+
10
+ # No need for unidiff import anymore as we're using block-based patching
11
+
12
+ MEMORY_FILE = "MEMORY.md"
13
+
14
+
15
+ mcp = FastMCP(
16
+ name="Project Memory MCP",
17
+ instructions=f"""
18
+ This MCP is for storing and retrieving project information to/from an English memory file, named
19
+ `{MEMORY_FILE}` in the project directory.
20
+
21
+ The memory file should store all information about the project in short and concise manner. It should be
22
+ good for humans and AI agents to catch up on the project status and progress quickly. Should contain descriptions,
23
+ ongoing tasks, tasks to do, discoveries, references to files and other project resources, URLs where to get more
24
+ information, etc.
25
+
26
+ Rules:
27
+ - This must be read by `get_project_memory` tool in the beginning of the first request of every conversation
28
+ if the conversation is about a project and a full project path is provided.
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.
32
+ - The `set_project_memory` tool must be used to set the whole project memory if `update_project_memory`
33
+ failed or there is no project memory yet.
34
+ - Never store any sensitive information in the memory file, e.g. personal information, company
35
+ information, passwords, access tokens, email addresses, etc!
36
+ - 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.
40
+ """
41
+ )
42
+
43
+ allowed_directories = []
44
+
45
+
46
+ def eprint(*args, **kwargs):
47
+ print(*args, file=sys.stderr, **kwargs)
48
+
49
+
50
+ def main():
51
+ # Process command line arguments
52
+ global allowed_directories
53
+ parser = argparse.ArgumentParser(description="Project Memory MCP server")
54
+ parser.add_argument(
55
+ '--allowed-dir',
56
+ action='append',
57
+ dest='allowed_dirs',
58
+ required=True,
59
+ help='Allowed base directory for project paths (can be used multiple times)'
60
+ )
61
+ args = parser.parse_args()
62
+ allowed_directories = [str(Path(d).resolve()) for d in args.allowed_dirs]
63
+
64
+ if not allowed_directories:
65
+ allowed_directories = [str(Path.home().resolve())]
66
+
67
+ eprint(f"Allowed directories: {allowed_directories}")
68
+
69
+ # Run the MCP server
70
+ mcp.run()
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()
75
+
76
+
77
+ #
78
+ # Tools
79
+ #
80
+
81
+ @mcp.tool()
82
+ def get_project_memory(
83
+ project_path: str = Field(description="The full path to the project directory")
84
+ ) -> str:
85
+ """
86
+ Get the whole project memory for the given project path in Markdown format.
87
+ 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
95
+ """
96
+ pp = Path(project_path).resolve()
97
+
98
+ # Check if the project path exists and is a directory
99
+ if not pp.exists() or not pp.is_dir():
100
+ raise FileNotFoundError(f"Project path {project_path} does not exist")
101
+ # Check if it is inside one of the allowed directories
102
+ if not any(str(pp).startswith(base) for base in allowed_directories):
103
+ raise PermissionError(f"Project path {project_path} is not in allowed directories")
104
+
105
+ with open(pp / MEMORY_FILE, "r") as f:
106
+ return f.read()
107
+
108
+
109
+ @mcp.tool()
110
+ def set_project_memory(
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")
113
+ ):
114
+ """
115
+ Set the whole project memory for the given project path in Markdown format.
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
131
+ """
132
+ pp = Path(project_path).resolve()
133
+ if not pp.exists() or not pp.is_dir():
134
+ raise FileNotFoundError(f"Project path {project_path} does not exist")
135
+ if not any(str(pp).startswith(base) for base in allowed_directories):
136
+ raise PermissionError(f"Project path {project_path} is not in allowed directories")
137
+
138
+ with open(pp / MEMORY_FILE, "w") as f:
139
+ f.write(project_info)
140
+
141
+
142
+ def validate_block_integrity(patch_content):
143
+ """
144
+ Validate the integrity of patch blocks before parsing.
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
156
+ """
157
+ # Check marker balance
158
+ search_count = patch_content.count("<<<<<<< SEARCH")
159
+ separator_count = patch_content.count("=======")
160
+ replace_count = patch_content.count(">>>>>>> REPLACE")
161
+
162
+ if not (search_count == separator_count == replace_count):
163
+ raise ValueError(
164
+ f"Malformed patch format: Unbalanced markers - "
165
+ f"{search_count} SEARCH, {separator_count} separator, {replace_count} REPLACE markers"
166
+ )
167
+
168
+ # Check marker sequence
169
+ markers = []
170
+ for line in patch_content.splitlines():
171
+ line = line.strip()
172
+ if line in ["<<<<<<< SEARCH", "=======", ">>>>>>> REPLACE"]:
173
+ markers.append(line)
174
+
175
+ # Verify correct marker sequence (always SEARCH, SEPARATOR, REPLACE pattern)
176
+ for i in range(0, len(markers), 3):
177
+ if i+2 < len(markers):
178
+ if markers[i] != "<<<<<<< SEARCH" or markers[i+1] != "=======" or markers[i+2] != ">>>>>>> REPLACE":
179
+ raise ValueError(
180
+ f"Malformed patch format: Incorrect marker sequence at position {i}: "
181
+ f"Expected [SEARCH, SEPARATOR, REPLACE], got {markers[i:i+3]}"
182
+ )
183
+
184
+ # Check for nested markers in each block
185
+ sections = patch_content.split("<<<<<<< SEARCH")
186
+ for i, section in enumerate(sections[1:], 1): # Skip first empty section
187
+ if "<<<<<<< SEARCH" in section and section.find(">>>>>>> REPLACE") > section.find("<<<<<<< SEARCH"):
188
+ raise ValueError(f"Malformed patch format: Nested SEARCH marker in block {i}")
189
+
190
+
191
+ def parse_search_replace_blocks(patch_content):
192
+ """
193
+ Parse multiple search-replace blocks from the patch content.
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
203
+ """
204
+ # Define the markers
205
+ search_marker = "<<<<<<< SEARCH"
206
+ separator = "======="
207
+ replace_marker = ">>>>>>> REPLACE"
208
+
209
+ # First validate patch integrity
210
+ validate_block_integrity(patch_content)
211
+
212
+ # Use regex to extract all blocks
213
+ pattern = f"{search_marker}\\n(.*?)\\n{separator}\\n(.*?)\\n{replace_marker}"
214
+ matches = re.findall(pattern, patch_content, re.DOTALL)
215
+
216
+ if not matches:
217
+ # Try alternative parsing if regex fails
218
+ blocks = []
219
+ lines = patch_content.splitlines()
220
+ i = 0
221
+ while i < len(lines):
222
+ if lines[i] == search_marker:
223
+ search_start = i + 1
224
+ separator_idx = -1
225
+ replace_end = -1
226
+
227
+ # Find the separator
228
+ for j in range(search_start, len(lines)):
229
+ if lines[j] == separator:
230
+ separator_idx = j
231
+ break
232
+
233
+ if separator_idx == -1:
234
+ raise ValueError("Invalid format: missing separator")
235
+
236
+ # Find the replace marker
237
+ for j in range(separator_idx + 1, len(lines)):
238
+ if lines[j] == replace_marker:
239
+ replace_end = j
240
+ break
241
+
242
+ if replace_end == -1:
243
+ raise ValueError("Invalid format: missing replace marker")
244
+
245
+ search_text = "\n".join(lines[search_start:separator_idx])
246
+ replace_text = "\n".join(lines[separator_idx + 1:replace_end])
247
+
248
+ # Check for markers in the search or replace text
249
+ if any(marker in search_text for marker in [search_marker, separator, replace_marker]):
250
+ raise ValueError(f"Block {len(blocks)+1}: Search text contains patch markers")
251
+ if any(marker in replace_text for marker in [search_marker, separator, replace_marker]):
252
+ raise ValueError(f"Block {len(blocks)+1}: Replace text contains patch markers")
253
+
254
+ blocks.append((search_text, replace_text))
255
+
256
+ i = replace_end + 1
257
+ else:
258
+ i += 1
259
+
260
+ if blocks:
261
+ return blocks
262
+ else:
263
+ raise ValueError("Invalid patch format. Expected block format with SEARCH/REPLACE markers.")
264
+
265
+ # Check for markers in matched content
266
+ for i, (search_text, replace_text) in enumerate(matches):
267
+ if any(marker in search_text for marker in [search_marker, separator, replace_marker]):
268
+ raise ValueError(f"Block {i+1}: Search text contains patch markers")
269
+ if any(marker in replace_text for marker in [search_marker, separator, replace_marker]):
270
+ raise ValueError(f"Block {i+1}: Replace text contains patch markers")
271
+
272
+ return matches
273
+
274
+
275
+ @mcp.tool()
276
+ def update_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")
279
+ ):
280
+ """
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
314
+ """
315
+ project_dir = Path(project_path).resolve()
316
+ if not project_dir.is_dir():
317
+ raise FileNotFoundError(f"Project path {project_path} does not exist or is not a directory")
318
+ memory_file = project_dir / MEMORY_FILE
319
+ if not memory_file.exists():
320
+ raise FileNotFoundError(
321
+ f"Memory file does not exist at {memory_file}. Use `set_project_memory` to set the whole memory instead."
322
+ )
323
+
324
+ # Read the current file content
325
+ with open(memory_file, 'r', encoding='utf-8') as f:
326
+ original_content = f.read()
327
+
328
+ try:
329
+ # First, try to parse as block format
330
+ try:
331
+ # Parse multiple search-replace blocks
332
+ blocks = parse_search_replace_blocks(patch_content)
333
+ if blocks:
334
+ eprint(f"Found {len(blocks)} search-replace blocks")
335
+
336
+ # Apply each block sequentially
337
+ current_content = original_content
338
+ applied_blocks = 0
339
+
340
+ for i, (search_text, replace_text) in enumerate(blocks):
341
+ eprint(f"Processing block {i+1}/{len(blocks)}")
342
+
343
+ # Check exact match count
344
+ count = current_content.count(search_text)
345
+
346
+ if count == 1:
347
+ # Exactly one match - perfect!
348
+ eprint(f"Block {i+1}: Found exactly one exact match")
349
+ current_content = current_content.replace(search_text, replace_text)
350
+ applied_blocks += 1
351
+ elif count > 1:
352
+ # Multiple matches - too ambiguous
353
+ raise ValueError(f"Block {i+1}: The search text appears {count} times in the file. "
354
+ "Please provide more context to identify the specific occurrence.")
355
+ else:
356
+ # No match found
357
+ raise ValueError(f"Block {i+1}: Could not find the search text in the file. "
358
+ "Please ensure the search text exactly matches the content in the file.")
359
+
360
+ # Write the final content back to the file
361
+ with open(memory_file, 'w', encoding='utf-8') as f:
362
+ f.write(current_content)
363
+
364
+ return f"Successfully applied {applied_blocks} patch blocks to memory file"
365
+ except Exception as block_error:
366
+ # If block format parsing fails, log the error and try traditional patch format
367
+ eprint(f"Block format parsing failed: {str(block_error)}")
368
+
369
+ # If you still want to support traditional patches with whatthepatch or similar, add that code here
370
+ # For now, we'll just raise the error from block parsing
371
+ raise block_error
372
+
373
+ except Exception as e:
374
+ # If anything goes wrong, provide detailed error
375
+ raise RuntimeError(f"Failed to apply patch: {str(e)}")
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: project-mem-mcp
3
+ Version: 0.1.0
4
+ Summary: An MCP Server to store and retreive project information from memory file
5
+ Author: PYNESYS LLC
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: fastmcp<3.0.0,>=2.2.0
11
+ Dynamic: license-file
12
+
13
+ # Project Memory MCP
14
+
15
+ An MCP Server to store and retrieve project information from memory file. This allows AI agents (like Claude) to maintain persistent memory about projects between conversations.
16
+
17
+ ## Overview
18
+
19
+ Project Memory MCP provides a simple way to:
20
+ - Store project information in Markdown format
21
+ - Retrieve project information at the beginning of conversations
22
+ - Update project information using patches
23
+
24
+ The memory is stored in a `MEMORY.md` file in each project directory.
25
+
26
+ ## Installation
27
+
28
+ ### Using uvx
29
+
30
+ This method uses `uvx` (from the `uv` Python package manager) to run the server without permanent installation:
31
+
32
+ #### Prerequisites
33
+
34
+ Install `uvx` from [uv](https://docs.astral.sh/uv/installation/) if you don't have it already.
35
+
36
+ #### Set up MCP Client (Claude Desktop, Cursor, etc.)
37
+
38
+ Merge the following config with your existing config file (e.g. `claude_desktop_config.json`):
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "project-memory": {
44
+ "command": "uvx",
45
+ "args": [
46
+ "project-mem-mcp",
47
+ "--allowed-dir", "/Users/your-username/projects",
48
+ "--allowed-dir", "/Users/your-username/Documents/code"
49
+ ]
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ > **Note:** Replace `/Users/your-username` with the actual path to your own projects and code directories.
56
+
57
+ ### Install from Source
58
+
59
+ #### Prerequisites
60
+
61
+ - Python 3.11 or higher
62
+ - Pip package manager
63
+
64
+ #### Clone the repository
65
+
66
+ ```bash
67
+ git clone https://github.com/your-username/project-mem-mcp.git
68
+ python -m venv venv
69
+ source venv/bin/activate
70
+ pip install -e .
71
+ ```
72
+
73
+ #### Set up MCP Client (Claude Desktop, Cursor, etc.)
74
+
75
+ Merge the following config with your existing config file (e.g. `claude_desktop_config.json`):
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "project-memory": {
81
+ "command": "path/to/your/venv/bin/project-mem-mcp",
82
+ "args": [
83
+ "--allowed-dir", "/Users/your-username/projects",
84
+ "--allowed-dir", "/Users/your-username/Documents/code"
85
+ ]
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ > **Note:** Replace `/Users/your-username` with the actual path to your own projects and code directories.
92
+
93
+ ## Arguments
94
+
95
+ The `--allowed-dir` argument is used to specify the directories that the server has access to. You can use it multiple times to allow access to multiple directories. All directories inside the allowed directories are also allowed.
96
+ It is optional. If not provided, the server will only have access to the home directory of the user running the server.
97
+
98
+
99
+ ## Usage
100
+
101
+ The MCP server is started by the client (e.g., Claude Desktop) based on the configuration you provide. You don't need to start the server manually.
102
+
103
+ ### Tools
104
+
105
+ Project Memory MCP provides three tools:
106
+
107
+ #### get_project_memory
108
+
109
+ Retrieves the entire project memory in Markdown format. Should be used at the beginning of every conversation about a project.
110
+
111
+ ```python
112
+ get_project_memory(project_path: str) -> str
113
+ ```
114
+ - **project_path**: Full path to the project directory.
115
+ - Returns the content of the MEMORY.md file as a string.
116
+ - Raises `FileNotFoundError` if the project or memory file does not exist.
117
+ - Raises `PermissionError` if the project path is not in allowed directories.
118
+
119
+ #### set_project_memory
120
+
121
+ Sets (overwrites) the entire project memory. Use this when creating a new memory file, replacing the whole memory, or if `update_project_memory` fails.
122
+
123
+ ```python
124
+ set_project_memory(project_path: str, project_info: str)
125
+ ```
126
+ - **project_path**: Full path to the project directory.
127
+ - **project_info**: Complete project information in Markdown format.
128
+ - Overwrites the MEMORY.md file with the provided content.
129
+ - Raises `FileNotFoundError` if the project path does not exist.
130
+ - Raises `PermissionError` if the project path is not in allowed directories.
131
+
132
+ #### update_project_memory
133
+
134
+ Updates the project memory by applying one or more block-based patches to the memory file. This is more efficient for small changes.
135
+
136
+ ```python
137
+ update_project_memory(project_path: str, patch_content: str)
138
+ ```
139
+ - **project_path**: Full path to the project directory.
140
+ - **patch_content**: Block-based patch content using SEARCH/REPLACE markers (see below).
141
+ - Each patch block must have the following format:
142
+
143
+ ```
144
+ <<<<<<< SEARCH
145
+ Text to find in the memory file
146
+ =======
147
+ Text to replace it with
148
+ >>>>>>> REPLACE
149
+ ```
150
+ Multiple blocks can be included in a single request.
151
+ - Each search text must appear **exactly once** in the file, otherwise an error is raised.
152
+ - Raises `FileNotFoundError` if the project or memory file does not exist.
153
+ - Raises `ValueError` if the patch format is invalid or the search text is not unique.
154
+ - Raises `RuntimeError` if patch application fails for any reason.
155
+
156
+ ### Example Workflow
157
+
158
+ 1. Begin a conversation with LLM about a project
159
+ 2. LLM uses `get_project_memory` to retrieve project information
160
+ 3. Throughout the conversation, LLM uses `update_project_memory` to persist new information
161
+ 4. If the update fails, LLM can use `set_project_memory` instead
162
+
163
+ #### Claude Desktop
164
+
165
+ if you use Claude Desktop, it is best to use the project feature.
166
+
167
+ Edit the project instructions:
168
+ - Add a line like this: "The path of the project is <project_path>"
169
+ - If it does not always use the memory, you can add a line like this: "Always use the project memory, it is not optional"
170
+
171
+ ## Security Considerations
172
+
173
+ - Memory files should never contain sensitive information
174
+ - Project paths are validated against allowed directories
175
+ - All file operations are restricted to allowed directories
176
+
177
+ ## Dependencies
178
+
179
+ - fastmcp (>=2.2.0, <3.0.0)
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/project_mem_mcp/__init__.py
5
+ src/project_mem_mcp/server.py
6
+ src/project_mem_mcp.egg-info/PKG-INFO
7
+ src/project_mem_mcp.egg-info/SOURCES.txt
8
+ src/project_mem_mcp.egg-info/dependency_links.txt
9
+ src/project_mem_mcp.egg-info/entry_points.txt
10
+ src/project_mem_mcp.egg-info/requires.txt
11
+ src/project_mem_mcp.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ project-mem-mcp = project_mem_mcp.server:main
@@ -0,0 +1 @@
1
+ fastmcp<3.0.0,>=2.2.0
@@ -0,0 +1 @@
1
+ project_mem_mcp