phone-a-friend-mcp-server 0.2.0__tar.gz → 0.3.0rc1__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.
Files changed (23) hide show
  1. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/PKG-INFO +18 -19
  2. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/README.md +17 -18
  3. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/pyproject.toml +1 -1
  4. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/tools/fax_tool.py +7 -6
  5. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/tools/phone_tool.py +7 -6
  6. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/utils/context_builder.py +36 -15
  7. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/tests/test_context_builder.py +62 -0
  8. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/tests/test_tools.py +53 -6
  9. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/uv.lock +1 -1
  10. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/.gitignore +0 -0
  11. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/.pre-commit-config.yaml +0 -0
  12. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/Dockerfile +0 -0
  13. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/LICENSE +0 -0
  14. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/__init__.py +0 -0
  15. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/__main__.py +0 -0
  16. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/client/__init__.py +0 -0
  17. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/config.py +0 -0
  18. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/server.py +0 -0
  19. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/tools/__init__.py +0 -0
  20. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/tools/base_tools.py +0 -0
  21. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/tools/tool_manager.py +0 -0
  22. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/src/phone_a_friend_mcp_server/utils/__init__.py +0 -0
  23. {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phone-a-friend-mcp-server
3
- Version: 0.2.0
3
+ Version: 0.3.0rc1
4
4
  Summary: MCP Server for Phone-a-Friend assistance
5
5
  Project-URL: GitHub, https://github.com/abhishekbhakat/phone-a-friend-mcp-server
6
6
  Project-URL: Issues, https://github.com/abhishekbhakat/phone-a-friend-mcp-server/issues
@@ -47,7 +47,7 @@ This enables AI systems to leverage other AI models as "consultants" for complex
47
47
  ## Architecture 🏗️
48
48
 
49
49
  ```
50
- Primary AI → Phone-a-Friend MCP → OpenRouter → External AI (GPT-4, Claude, etc.) → Processed Response → Primary AI
50
+ Primary AI → Phone-a-Friend MCP → OpenRouter → External AI (O3, Claude, etc.) → Processed Response → Primary AI
51
51
  ```
52
52
 
53
53
  **Sequential Workflow:**
@@ -71,22 +71,21 @@ The `uv` runner will automatically download and execute the server package if it
71
71
 
72
72
  Add the following JSON configuration to your MCP client and replace `<YOUR_API_KEY>` with your key:
73
73
 
74
- ```json
75
- {
76
- "mcpServers": {
77
- "phone-a-friend": {
78
- "command": "uv",
79
- "args": [
80
- "run",
81
- "phone-a-friend-mcp-server",
82
- "--provider", "openai",
83
- "--api-key", "<YOUR_API_KEY>"
84
- ]
85
- }
86
- }
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "phone-a-friend": {
78
+ "command": "uvx",
79
+ "args": [
80
+ "phone-a-friend-mcp-server",
81
+ "--provider", "openai",
82
+ "--api-key", "<YOUR_API_KEY>"
83
+ ]
87
84
  }
88
- ```
89
- > That's it! You can now use the `phone_a_friend` tool in any compatible client. For more options, see the Advanced Configuration section.
85
+ }
86
+ }
87
+ ```
88
+ > That's it! You can now use the `phone_a_friend` tool in any compatible client. For more options, see the Advanced Configuration section.
90
89
 
91
90
  ## Available Tools 🛠️
92
91
 
@@ -150,12 +149,12 @@ You can override the default model for each provider.
150
149
 
151
150
  **Override with CLI:**
152
151
  ```bash
153
- phone-a-friend-mcp-server --model "gpt-4-turbo"
152
+ phone-a-friend-mcp-server --model "o3"
154
153
  ```
155
154
 
156
155
  **Override with Environment Variable:**
157
156
  ```bash
158
- export PHONE_A_FRIEND_MODEL="gpt-4-turbo"
157
+ export PHONE_A_FRIEND_MODEL="o3"
159
158
  ```
160
159
 
161
160
  ### Additional Options
@@ -22,7 +22,7 @@ This enables AI systems to leverage other AI models as "consultants" for complex
22
22
  ## Architecture 🏗️
23
23
 
24
24
  ```
25
- Primary AI → Phone-a-Friend MCP → OpenRouter → External AI (GPT-4, Claude, etc.) → Processed Response → Primary AI
25
+ Primary AI → Phone-a-Friend MCP → OpenRouter → External AI (O3, Claude, etc.) → Processed Response → Primary AI
26
26
  ```
27
27
 
28
28
  **Sequential Workflow:**
@@ -46,22 +46,21 @@ The `uv` runner will automatically download and execute the server package if it
46
46
 
47
47
  Add the following JSON configuration to your MCP client and replace `<YOUR_API_KEY>` with your key:
48
48
 
49
- ```json
50
- {
51
- "mcpServers": {
52
- "phone-a-friend": {
53
- "command": "uv",
54
- "args": [
55
- "run",
56
- "phone-a-friend-mcp-server",
57
- "--provider", "openai",
58
- "--api-key", "<YOUR_API_KEY>"
59
- ]
60
- }
61
- }
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "phone-a-friend": {
53
+ "command": "uvx",
54
+ "args": [
55
+ "phone-a-friend-mcp-server",
56
+ "--provider", "openai",
57
+ "--api-key", "<YOUR_API_KEY>"
58
+ ]
62
59
  }
63
- ```
64
- > That's it! You can now use the `phone_a_friend` tool in any compatible client. For more options, see the Advanced Configuration section.
60
+ }
61
+ }
62
+ ```
63
+ > That's it! You can now use the `phone_a_friend` tool in any compatible client. For more options, see the Advanced Configuration section.
65
64
 
66
65
  ## Available Tools 🛠️
67
66
 
@@ -125,12 +124,12 @@ You can override the default model for each provider.
125
124
 
126
125
  **Override with CLI:**
127
126
  ```bash
128
- phone-a-friend-mcp-server --model "gpt-4-turbo"
127
+ phone-a-friend-mcp-server --model "o3"
129
128
  ```
130
129
 
131
130
  **Override with Environment Variable:**
132
131
  ```bash
133
- export PHONE_A_FRIEND_MODEL="gpt-4-turbo"
132
+ export PHONE_A_FRIEND_MODEL="o3"
134
133
  ```
135
134
 
136
135
  ### Additional Options
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "phone-a-friend-mcp-server"
3
- version = "0.2.0"
3
+ version = "0.3.0rc1"
4
4
  description = "MCP Server for Phone-a-Friend assistance"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -85,17 +85,18 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
85
85
  "all_related_context": {
86
86
  "type": "string",
87
87
  "description": (
88
- "MANDATORY. General, non-code context for the friend AI. "
89
- "Include known constraints (Python version, allowed deps, etc.), "
90
- "failing test output, or tracebacks. DO NOT include file contents here."
88
+ "General context for the friend AI. Include known constraints "
89
+ "(Python version, allowed deps, etc.), failing test output, tracebacks, "
90
+ "or code snippets for reference. For complete files, use file_list instead."
91
91
  ),
92
92
  },
93
93
  "file_list": {
94
94
  "type": "array",
95
95
  "items": {"type": "string"},
96
96
  "description": (
97
- "MANDATORY. A list of file paths or glob patterns to be included in the code context. "
98
- "The tool will automatically read these files, filter them against .gitignore, and build the context."
97
+ "Optional but recommended. A list of file paths or glob patterns to be included in the code context. "
98
+ "The tool will automatically read these files, filter them against .gitignore, and build the context. "
99
+ "Better and faster than including complete files in all_related_context."
99
100
  ),
100
101
  },
101
102
  "task": {
@@ -119,7 +120,7 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
119
120
  ),
120
121
  },
121
122
  },
122
- "required": ["all_related_context", "file_list", "task", "output_directory"],
123
+ "required": ["all_related_context", "task", "output_directory"],
123
124
  }
124
125
 
125
126
  async def run(self, **kwargs) -> dict[str, Any]:
@@ -87,17 +87,18 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
87
87
  "all_related_context": {
88
88
  "type": "string",
89
89
  "description": (
90
- "MANDATORY. General, non-code context for the friend AI. "
91
- "Include known constraints (Python version, allowed deps, etc.), "
92
- "failing test output, or tracebacks. DO NOT include file contents here."
90
+ "General context for the friend AI. Include known constraints "
91
+ "(Python version, allowed deps, etc.), failing test output, tracebacks, "
92
+ "or code snippets for reference. For complete files, use file_list instead."
93
93
  ),
94
94
  },
95
95
  "file_list": {
96
96
  "type": "array",
97
97
  "items": {"type": "string"},
98
98
  "description": (
99
- "MANDATORY. A list of file paths or glob patterns to be included in the code context. "
100
- "The tool will automatically read these files, filter them against .gitignore, and build the context."
99
+ "Optional but recommended. A list of file paths or glob patterns to be included in the code context. "
100
+ "The tool will automatically read these files, filter them against .gitignore, and build the context. "
101
+ "Better and faster than including complete files in all_related_context."
101
102
  ),
102
103
  },
103
104
  "task": {
@@ -112,7 +113,7 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
112
113
  ),
113
114
  },
114
115
  },
115
- "required": ["all_related_context", "file_list", "task"],
116
+ "required": ["all_related_context", "task"],
116
117
  }
117
118
 
118
119
  async def run(self, **kwargs) -> dict[str, Any]:
@@ -14,6 +14,20 @@ def load_gitignore(base_dir: str) -> pathspec.PathSpec:
14
14
  return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
15
15
 
16
16
 
17
+ def get_all_project_files(base_dir: str = ".") -> list[str]:
18
+ """Get all files in the project directory recursively."""
19
+ all_files = []
20
+ for root, dirs, files in os.walk(base_dir):
21
+ # Skip hidden directories like .git, .venv, etc.
22
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
23
+
24
+ for file in files:
25
+ if not file.startswith('.'): # Skip hidden files
26
+ rel_path = os.path.relpath(os.path.join(root, file), base_dir)
27
+ all_files.append(rel_path)
28
+ return sorted(all_files)
29
+
30
+
17
31
  def filter_paths(paths: list[str], spec: pathspec.PathSpec, base_dir: str = ".") -> list[str]:
18
32
  """Filters out paths that match the .gitignore spec and non-text files."""
19
33
  filtered_paths = []
@@ -76,18 +90,25 @@ def build_code_context(file_list: list[str], base_dir: str = ".") -> str:
76
90
  """
77
91
  spec = load_gitignore(base_dir)
78
92
 
79
- all_files = []
80
- for pattern in file_list:
81
- all_files.extend(glob.glob(pattern, recursive=True))
82
-
83
- unique_files = sorted(list(set(all_files)))
84
-
85
- filtered_files = filter_paths(unique_files, spec, base_dir)
86
-
87
- if not filtered_files:
88
- return "No files to display. Check your `file_list` and `.gitignore`."
89
-
90
- file_tree = build_file_tree(filtered_files, base_dir)
91
- file_blocks = build_file_blocks(filtered_files, base_dir)
92
-
93
- return f"<file_tree>\n{file_tree}\n</file_tree>\n\n{file_blocks}"
93
+ # Get complete project tree (like tree --gitignore)
94
+ all_project_files = get_all_project_files(base_dir)
95
+ filtered_all_files = filter_paths(all_project_files, spec, base_dir)
96
+ complete_tree = build_file_tree(filtered_all_files, base_dir)
97
+
98
+ # Handle selected files from file_list
99
+ if file_list:
100
+ selected_files = []
101
+ for pattern in file_list:
102
+ selected_files.extend(glob.glob(pattern, recursive=True))
103
+
104
+ unique_selected_files = sorted(list(set(selected_files)))
105
+ filtered_selected_files = filter_paths(unique_selected_files, spec, base_dir)
106
+
107
+ if filtered_selected_files:
108
+ file_blocks = build_file_blocks(filtered_selected_files, base_dir)
109
+ return f"<file_tree>\n{complete_tree}\n</file_tree>\n\n{file_blocks}"
110
+ else:
111
+ return f"<file_tree>\n{complete_tree}\n</file_tree>\n\nNo files to display from file_list. Check your patterns and .gitignore."
112
+ else:
113
+ # No file_list provided, just show the tree
114
+ return f"<file_tree>\n{complete_tree}\n</file_tree>\n\nNo specific files selected. Use file_list parameter to include file contents."
@@ -8,6 +8,7 @@ from phone_a_friend_mcp_server.utils.context_builder import (
8
8
  build_file_blocks,
9
9
  build_file_tree,
10
10
  filter_paths,
11
+ get_all_project_files,
11
12
  load_gitignore,
12
13
  )
13
14
 
@@ -103,3 +104,64 @@ def test_build_code_context(temp_project):
103
104
  assert "print('hello')" in context
104
105
  assert '<file="README.md">' in context
105
106
  assert "# Project" in context
107
+
108
+
109
+ def test_get_all_project_files(temp_project):
110
+ """Test getting all project files."""
111
+ original_cwd = os.getcwd()
112
+ os.chdir(temp_project)
113
+ try:
114
+ all_files = get_all_project_files(".")
115
+ finally:
116
+ os.chdir(original_cwd)
117
+
118
+ # Should include all non-hidden files
119
+ assert "src/main.py" in all_files
120
+ assert "src/main.pyc" in all_files
121
+ assert "README.md" in all_files
122
+ assert "dist/package.tar.gz" in all_files
123
+ assert "app.log" in all_files
124
+ # Should not include hidden files or directories
125
+ assert ".gitignore" not in all_files
126
+
127
+
128
+ def test_build_code_context_empty_file_list(temp_project):
129
+ """Test build_code_context with empty file_list."""
130
+ original_cwd = os.getcwd()
131
+ os.chdir(temp_project)
132
+ try:
133
+ context = build_code_context([], base_dir=".")
134
+ finally:
135
+ os.chdir(original_cwd)
136
+
137
+ # Should still show complete tree
138
+ assert "<file_tree>" in context
139
+ assert "main.py" in context
140
+ assert "README.md" in context
141
+ # Should not include ignored files in tree
142
+ assert "main.pyc" not in context
143
+ assert "dist" not in context
144
+ assert "app.log" not in context
145
+ # Should not include file contents
146
+ assert '<file="src/main.py">' not in context
147
+ assert "No specific files selected" in context
148
+
149
+
150
+ def test_build_code_context_complete_tree_always_shown(temp_project):
151
+ """Test that complete tree is always shown regardless of file_list."""
152
+ original_cwd = os.getcwd()
153
+ os.chdir(temp_project)
154
+ try:
155
+ # Test with only one file in file_list
156
+ context = build_code_context(["README.md"], base_dir=".")
157
+ finally:
158
+ os.chdir(original_cwd)
159
+
160
+ # Should show complete tree (including files not in file_list)
161
+ assert "<file_tree>" in context
162
+ assert "main.py" in context # This file is in tree but not in file_list
163
+ assert "README.md" in context
164
+ # Should only include content for files in file_list
165
+ assert '<file="README.md">' in context
166
+ assert "# Project" in context
167
+ assert '<file="src/main.py">' not in context # Content not included
@@ -5,6 +5,7 @@ import pytest
5
5
 
6
6
  from phone_a_friend_mcp_server.config import PhoneAFriendConfig
7
7
  from phone_a_friend_mcp_server.tools.fax_tool import FaxAFriendTool
8
+ from phone_a_friend_mcp_server.tools.phone_tool import PhoneAFriendTool
8
9
 
9
10
 
10
11
  @pytest.fixture
@@ -68,12 +69,58 @@ async def test_fax_a_friend_tool_with_context_builder(config, temp_project):
68
69
  assert "<file_tree>" in content
69
70
  assert "main.py" in content
70
71
  assert "README.md" in content
71
- assert "main.pyc" not in content # Should be ignored
72
- assert '<file="src/main.py">' in content
73
- assert "print('hello')" in content
74
- assert "=== INSTRUCTIONS ===" in content
75
- assert "Provide exhaustive, step-by-step reasoning." in content
76
- assert "Never include files matching .gitignore patterns." in content
72
+
73
+
74
+ @pytest.mark.asyncio
75
+ async def test_fax_a_friend_tool_without_file_list(config, temp_project):
76
+ """Test the fax a friend tool without file_list parameter."""
77
+ tool = FaxAFriendTool(config)
78
+
79
+ result = await tool.run(
80
+ all_related_context="This is the general context with code snippets.",
81
+ task="Review the architecture.",
82
+ output_directory=".",
83
+ )
84
+
85
+ assert result["status"] == "success"
86
+ assert result["file_name"] == "fax_a_friend.md"
87
+
88
+ expected_file_path = os.path.join(temp_project, "fax_a_friend.md")
89
+ assert os.path.exists(expected_file_path)
90
+
91
+ with open(expected_file_path, encoding="utf-8") as f:
92
+ content = f.read()
93
+ assert "=== TASK ===" in content
94
+ assert "Review the architecture." in content
95
+ assert "=== GENERAL CONTEXT ===" in content
96
+ assert "This is the general context with code snippets." in content
97
+ assert "=== CODE CONTEXT ===" in content
98
+ assert "<file_tree>" in content
99
+ # Should show complete tree but no file contents
100
+ assert "No specific files selected" in content
101
+
102
+
103
+ @pytest.mark.asyncio
104
+ async def test_phone_a_friend_tool_without_file_list(config):
105
+ """Test the phone a friend tool without file_list parameter."""
106
+ tool = PhoneAFriendTool(config)
107
+
108
+ # Mock the agent to avoid actual API calls
109
+ class MockAgent:
110
+ async def run(self, prompt, model_settings=None):
111
+ class MockResult:
112
+ data = "Mock AI response"
113
+ return MockResult()
114
+
115
+ tool._create_agent = lambda: MockAgent()
116
+
117
+ result = await tool.run(
118
+ all_related_context="This is the general context with code snippets.",
119
+ task="Review the architecture.",
120
+ )
121
+
122
+ assert result["status"] == "success"
123
+ assert result["response"] == "Mock AI response"
77
124
 
78
125
 
79
126
  def test_config_default_temperature():
@@ -729,7 +729,7 @@ wheels = [
729
729
 
730
730
  [[package]]
731
731
  name = "phone-a-friend-mcp-server"
732
- version = "0.2.0"
732
+ version = "0.3.0rc1"
733
733
  source = { editable = "." }
734
734
  dependencies = [
735
735
  { name = "aiofiles" },