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.
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/PKG-INFO +18 -19
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/README.md +17 -18
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/pyproject.toml +1 -1
- {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
- {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
- {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
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/tests/test_context_builder.py +62 -0
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/tests/test_tools.py +53 -6
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/uv.lock +1 -1
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/.gitignore +0 -0
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/.pre-commit-config.yaml +0 -0
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/Dockerfile +0 -0
- {phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/LICENSE +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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.
|
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 (
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
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 "
|
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="
|
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 (
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
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 "
|
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="
|
132
|
+
export PHONE_A_FRIEND_MODEL="o3"
|
134
133
|
```
|
135
134
|
|
136
135
|
### Additional Options
|
@@ -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
|
-
"
|
89
|
-
"
|
90
|
-
"
|
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
|
-
"
|
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", "
|
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
|
-
"
|
91
|
-
"
|
92
|
-
"
|
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
|
-
"
|
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", "
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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."
|
{phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/tests/test_context_builder.py
RENAMED
@@ -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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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():
|
File without changes
|
{phone_a_friend_mcp_server-0.2.0 → phone_a_friend_mcp_server-0.3.0rc1}/.pre-commit-config.yaml
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|