Skip to content

Commit 8af419d

Browse files
committed
feat: serialize non-string content using json.dumps
When the content parameter to codemcp in codemcp/main.py is not a string, instead of rejecting it, let's serialize it to a string using json.dumps and then proceed. Add a test for this with WriteFile; note that to trigger argument validation logic, you MUST set in_process = False on the test class, make a dedicated test class for this test, and verify that your test fails without your code change. ```git-revs 440a7e7 (Base revision) a1d8db6 Serialize non-string content using json.dumps for WriteFile subtool d732993 Add test for serializing non-string content to JSON 0f487bb Add script to test failure without our code change 8225c05 Add script to restore original code 3babc32 Change content parameter type to accept any object 84cfe83 Update content parameter description in init_project.py e57f442 Auto-commit format changes HEAD Auto-commit lint changes ``` codemcp-id: 0-feat-serialize-non-string-content-using-json-dumps ghstack-source-id: 5e50a4a Pull-Request-resolved: #202
1 parent 4f9a5ce commit 8af419d

File tree

3 files changed

+110
-3
lines changed

3 files changed

+110
-3
lines changed

codemcp/main.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async def codemcp(
2828
subtool: str,
2929
*,
3030
path: str | None = None,
31-
content: str | None = None,
31+
content: object = None, # Allow any type, will be serialized to string if needed
3232
old_string: str | None = None,
3333
new_string: str | None = None,
3434
offset: int | None = None,
@@ -178,7 +178,14 @@ def normalize_newlines(s):
178178
if description is None:
179179
raise ValueError("description is required for WriteFile subtool")
180180

181-
content_str = content or ""
181+
import json
182+
183+
# If content is not a string, serialize it to a string using json.dumps
184+
if content is not None and not isinstance(content, str):
185+
content_str = json.dumps(content)
186+
else:
187+
content_str = content or ""
188+
182189
return await write_file_content(path, content_str, description, chat_id)
183190

184191
if subtool == "EditFile":

codemcp/tools/init_project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ async def init_project(
473473
Args:
474474
subtool: The subtool to execute (ReadFile, WriteFile, EditFile, LS, InitProject, UserPrompt, RunCommand, RM, Think, Chmod)
475475
path: The path to the file or directory to operate on
476-
content: Content for WriteFile subtool
476+
content: Content for WriteFile subtool (any type will be serialized to string if needed)
477477
old_string: String to replace for EditFile subtool
478478
new_string: Replacement string for EditFile subtool
479479
offset: Line offset for ReadFile subtool
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env python3
2+
3+
"""Tests for serializing non-string content using json.dumps."""
4+
5+
import json
6+
import os
7+
import unittest
8+
9+
from codemcp.testing import MCPEndToEndTestCase
10+
11+
12+
class JsonContentSerializationTest(MCPEndToEndTestCase):
13+
"""Test the serialization of non-string content for WriteFile subtool."""
14+
15+
# Set in_process = False to ensure argument validation logic is triggered
16+
in_process = False
17+
18+
async def test_json_serialization(self):
19+
"""Test that non-string content is properly serialized to JSON."""
20+
test_file_path = os.path.join(self.temp_dir.name, "json_serialized.txt")
21+
22+
# Dictionary to be serialized
23+
content_dict = {
24+
"name": "Test Object",
25+
"values": [1, 2, 3],
26+
"nested": {"key": "value", "boolean": True},
27+
}
28+
29+
# Expected serialized string for verification
30+
expected_content = json.dumps(content_dict)
31+
32+
async with self.create_client_session() as session:
33+
# First initialize project to get chat_id
34+
init_result_text = await self.call_tool_assert_success(
35+
session,
36+
"codemcp",
37+
{
38+
"subtool": "InitProject",
39+
"path": self.temp_dir.name,
40+
"user_prompt": "Test initialization for JSON serialization test",
41+
"subject_line": "test: test json content serialization",
42+
"reuse_head_chat_id": False,
43+
},
44+
)
45+
46+
# Extract chat_id from the init result
47+
chat_id = self.extract_chat_id_from_text(init_result_text)
48+
49+
# Call the WriteFile tool with a dict as content
50+
result_text = await self.call_tool_assert_success(
51+
session,
52+
"codemcp",
53+
{
54+
"subtool": "WriteFile",
55+
"path": test_file_path,
56+
"content": content_dict, # Passing a dictionary instead of a string
57+
"description": "Create file with JSON serialized content",
58+
"chat_id": chat_id,
59+
},
60+
)
61+
62+
# Verify the success message
63+
self.assertIn("Successfully wrote to", result_text)
64+
65+
# Verify the file was created with the correct content
66+
with open(test_file_path) as f:
67+
file_content = f.read()
68+
69+
self.assertEqual(file_content, expected_content)
70+
71+
# Test with a list
72+
content_list = [1, "two", 3.0, False, None]
73+
expected_list_content = json.dumps(content_list)
74+
list_file_path = os.path.join(self.temp_dir.name, "list_serialized.txt")
75+
76+
# Call WriteFile with a list as content
77+
result_text = await self.call_tool_assert_success(
78+
session,
79+
"codemcp",
80+
{
81+
"subtool": "WriteFile",
82+
"path": list_file_path,
83+
"content": content_list, # Passing a list instead of a string
84+
"description": "Create file with list content",
85+
"chat_id": chat_id,
86+
},
87+
)
88+
89+
# Verify the success message
90+
self.assertIn("Successfully wrote to", result_text)
91+
92+
# Verify the file was created with the correct content
93+
with open(list_file_path) as f:
94+
file_content = f.read()
95+
96+
self.assertEqual(file_content, expected_list_content)
97+
98+
99+
if __name__ == "__main__":
100+
unittest.main()

0 commit comments

Comments
 (0)