phone-a-friend-mcp-server 0.1.2__py3-none-any.whl → 0.3.0rc1__py3-none-any.whl
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/config.py +1 -0
- phone_a_friend_mcp_server/tools/fax_tool.py +24 -36
- phone_a_friend_mcp_server/tools/phone_tool.py +24 -29
- phone_a_friend_mcp_server/utils/__init__.py +1 -0
- phone_a_friend_mcp_server/utils/context_builder.py +114 -0
- {phone_a_friend_mcp_server-0.1.2.dist-info → phone_a_friend_mcp_server-0.3.0rc1.dist-info}/METADATA +79 -109
- phone_a_friend_mcp_server-0.3.0rc1.dist-info/RECORD +17 -0
- phone_a_friend_mcp_server-0.1.2.dist-info/RECORD +0 -15
- {phone_a_friend_mcp_server-0.1.2.dist-info → phone_a_friend_mcp_server-0.3.0rc1.dist-info}/WHEEL +0 -0
- {phone_a_friend_mcp_server-0.1.2.dist-info → phone_a_friend_mcp_server-0.3.0rc1.dist-info}/entry_points.txt +0 -0
- {phone_a_friend_mcp_server-0.1.2.dist-info → phone_a_friend_mcp_server-0.3.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -4,6 +4,7 @@ from typing import Any
|
|
4
4
|
import aiofiles
|
5
5
|
|
6
6
|
from phone_a_friend_mcp_server.tools.base_tools import BaseTool
|
7
|
+
from phone_a_friend_mcp_server.utils.context_builder import build_code_context
|
7
8
|
|
8
9
|
|
9
10
|
class FaxAFriendTool(BaseTool):
|
@@ -35,9 +36,10 @@ refactors, design, migrations.
|
|
35
36
|
|
36
37
|
This tool creates a file for manual AI consultation. After file creation,
|
37
38
|
wait for the user to return with the external AI's response.
|
39
|
+
Replies must be exhaustively detailed. Do **NOT** include files ignored by .gitignore (e.g., *.pyc).
|
38
40
|
|
39
41
|
Hard restrictions:
|
40
|
-
• Generated prompt includes *only* the
|
42
|
+
• Generated prompt includes *only* the context you provide.
|
41
43
|
• No memory, no internet, no tools.
|
42
44
|
• You must spell out every fact it should rely on.
|
43
45
|
|
@@ -83,22 +85,18 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
83
85
|
"all_related_context": {
|
84
86
|
"type": "string",
|
85
87
|
"description": (
|
86
|
-
"
|
87
|
-
"
|
88
|
-
|
89
|
-
"- Known constraints (Python version, allowed deps, runtime limits, etc.).\n"
|
90
|
-
"- Any failing test output or traceback.\n"
|
91
|
-
"If it's not here, the friend AI can't use it."
|
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."
|
92
91
|
),
|
93
92
|
},
|
94
|
-
"
|
95
|
-
"type": "
|
93
|
+
"file_list": {
|
94
|
+
"type": "array",
|
95
|
+
"items": {"type": "string"},
|
96
96
|
"description": (
|
97
|
-
"Optional
|
98
|
-
"
|
99
|
-
"
|
100
|
-
"- Similar past solutions or reference snippets.\n"
|
101
|
-
"Skip it if there's nothing useful."
|
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."
|
102
100
|
),
|
103
101
|
},
|
104
102
|
"task": {
|
@@ -127,24 +125,21 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
127
125
|
|
128
126
|
async def run(self, **kwargs) -> dict[str, Any]:
|
129
127
|
all_related_context = kwargs.get("all_related_context", "")
|
130
|
-
|
128
|
+
file_list = kwargs.get("file_list", [])
|
131
129
|
task = kwargs.get("task", "")
|
132
130
|
output_directory = kwargs.get("output_directory", "")
|
133
131
|
|
134
|
-
|
135
|
-
master_prompt = self._create_master_prompt(all_related_context,
|
132
|
+
code_context = build_code_context(file_list)
|
133
|
+
master_prompt = self._create_master_prompt(all_related_context, code_context, task)
|
136
134
|
|
137
135
|
try:
|
138
|
-
# Validate and prepare output directory
|
139
136
|
output_dir = self._prepare_output_directory(output_directory)
|
140
137
|
|
141
|
-
# Create full file path
|
142
138
|
file_path = os.path.join(output_dir, "fax_a_friend.md")
|
143
139
|
|
144
140
|
async with aiofiles.open(file_path, "w", encoding="utf-8") as f:
|
145
141
|
await f.write(master_prompt)
|
146
142
|
|
147
|
-
# Get absolute path for user reference
|
148
143
|
abs_path = os.path.abspath(file_path)
|
149
144
|
|
150
145
|
return {
|
@@ -153,15 +148,15 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
153
148
|
"file_name": "fax_a_friend.md",
|
154
149
|
"output_directory": output_dir,
|
155
150
|
"prompt_length": len(master_prompt),
|
156
|
-
"context_length": len(
|
151
|
+
"context_length": len(master_prompt),
|
157
152
|
"task": task,
|
158
153
|
"instructions": self._get_manual_workflow_instructions(abs_path),
|
159
154
|
}
|
160
155
|
|
161
156
|
except Exception as e:
|
162
|
-
return {"status": "failed", "error": str(e), "output_directory": output_directory, "context_length": len(
|
157
|
+
return {"status": "failed", "error": str(e), "output_directory": output_directory, "context_length": len(master_prompt), "task": task}
|
163
158
|
|
164
|
-
def _create_master_prompt(self, all_related_context: str,
|
159
|
+
def _create_master_prompt(self, all_related_context: str, code_context: str, task: str) -> str:
|
165
160
|
"""Create a comprehensive prompt identical to PhoneAFriendTool's version."""
|
166
161
|
|
167
162
|
prompt_parts = [
|
@@ -171,23 +166,19 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
171
166
|
"=== TASK ===",
|
172
167
|
task,
|
173
168
|
"",
|
174
|
-
"===
|
169
|
+
"=== GENERAL CONTEXT ===",
|
175
170
|
all_related_context,
|
171
|
+
"",
|
172
|
+
"=== CODE CONTEXT ===",
|
173
|
+
code_context,
|
176
174
|
]
|
177
175
|
|
178
|
-
if any_additional_context.strip():
|
179
|
-
prompt_parts.extend(
|
180
|
-
[
|
181
|
-
"",
|
182
|
-
"=== ADDITIONAL CONTEXT ===",
|
183
|
-
any_additional_context,
|
184
|
-
]
|
185
|
-
)
|
186
|
-
|
187
176
|
prompt_parts.extend(
|
188
177
|
[
|
189
178
|
"",
|
190
179
|
"=== INSTRUCTIONS ===",
|
180
|
+
"- Provide exhaustive, step-by-step reasoning.",
|
181
|
+
"- Never include files matching .gitignore patterns.",
|
191
182
|
"- Analyze the code and requirements step-by-step.",
|
192
183
|
"- Show your reasoning and propose concrete changes.",
|
193
184
|
'- Provide updated code using the XML format (<file_tree> plus <file="…"> blocks).',
|
@@ -204,17 +195,14 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
204
195
|
if not output_directory:
|
205
196
|
raise ValueError("output_directory parameter is required")
|
206
197
|
|
207
|
-
# Expand user path (~) and resolve relative paths
|
208
198
|
expanded_path = os.path.expanduser(output_directory)
|
209
199
|
resolved_path = os.path.abspath(expanded_path)
|
210
200
|
|
211
|
-
# Create directory if it doesn't exist
|
212
201
|
try:
|
213
202
|
os.makedirs(resolved_path, exist_ok=True)
|
214
203
|
except OSError as e:
|
215
204
|
raise ValueError(f"Cannot create directory '{resolved_path}': {e}")
|
216
205
|
|
217
|
-
# Check if directory is writable
|
218
206
|
if not os.access(resolved_path, os.W_OK):
|
219
207
|
raise ValueError(f"Directory '{resolved_path}' is not writable")
|
220
208
|
|
@@ -10,6 +10,7 @@ from pydantic_ai.providers.openai import OpenAIProvider
|
|
10
10
|
from pydantic_ai.providers.openrouter import OpenRouterProvider
|
11
11
|
|
12
12
|
from phone_a_friend_mcp_server.tools.base_tools import BaseTool
|
13
|
+
from phone_a_friend_mcp_server.utils.context_builder import build_code_context
|
13
14
|
|
14
15
|
|
15
16
|
class PhoneAFriendTool(BaseTool):
|
@@ -39,9 +40,10 @@ Purpose: pair-programming caliber *coding help* — reviews, debugging,
|
|
39
40
|
refactors, design, migrations.
|
40
41
|
|
41
42
|
Hard restrictions:
|
42
|
-
• Friend AI sees *only* the
|
43
|
+
• Friend AI sees *only* the context you provide.
|
43
44
|
• No memory, no internet, no tools.
|
44
45
|
• You must spell out every fact it should rely on.
|
46
|
+
Replies must be exhaustively detailed. Do **NOT** include files ignored by .gitignore (e.g., *.pyc).
|
45
47
|
|
46
48
|
Required I/O format:
|
47
49
|
```
|
@@ -85,22 +87,18 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
85
87
|
"all_related_context": {
|
86
88
|
"type": "string",
|
87
89
|
"description": (
|
88
|
-
"
|
89
|
-
"
|
90
|
-
|
91
|
-
"- Known constraints (Python version, allowed deps, runtime limits, etc.).\n"
|
92
|
-
"- Any failing test output or traceback.\n"
|
93
|
-
"If it's not here, the friend AI can't use it."
|
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."
|
94
93
|
),
|
95
94
|
},
|
96
|
-
"
|
97
|
-
"type": "
|
95
|
+
"file_list": {
|
96
|
+
"type": "array",
|
97
|
+
"items": {"type": "string"},
|
98
98
|
"description": (
|
99
|
-
"Optional
|
100
|
-
"
|
101
|
-
"
|
102
|
-
"- Similar past solutions or reference snippets.\n"
|
103
|
-
"Skip it if there's nothing useful."
|
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."
|
104
102
|
),
|
105
103
|
},
|
106
104
|
"task": {
|
@@ -120,10 +118,11 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
120
118
|
|
121
119
|
async def run(self, **kwargs) -> dict[str, Any]:
|
122
120
|
all_related_context = kwargs.get("all_related_context", "")
|
123
|
-
|
121
|
+
file_list = kwargs.get("file_list", [])
|
124
122
|
task = kwargs.get("task", "")
|
125
123
|
|
126
|
-
|
124
|
+
code_context = build_code_context(file_list)
|
125
|
+
master_prompt = self._create_master_prompt(all_related_context, code_context, task)
|
127
126
|
|
128
127
|
try:
|
129
128
|
agent = self._create_agent()
|
@@ -140,7 +139,7 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
140
139
|
"provider": self.config.provider,
|
141
140
|
"model": self.config.model,
|
142
141
|
"temperature": temperature,
|
143
|
-
"context_length": len(
|
142
|
+
"context_length": len(master_prompt),
|
144
143
|
"task": task,
|
145
144
|
}
|
146
145
|
|
@@ -152,7 +151,7 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
152
151
|
"provider": self.config.provider,
|
153
152
|
"model": self.config.model,
|
154
153
|
"temperature": temperature,
|
155
|
-
"context_length": len(
|
154
|
+
"context_length": len(master_prompt),
|
156
155
|
"task": task,
|
157
156
|
"master_prompt": master_prompt,
|
158
157
|
}
|
@@ -182,7 +181,7 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
182
181
|
|
183
182
|
return Agent(model)
|
184
183
|
|
185
|
-
def _create_master_prompt(self, all_related_context: str,
|
184
|
+
def _create_master_prompt(self, all_related_context: str, code_context: str, task: str) -> str:
|
186
185
|
"""Create a comprehensive prompt for the external AI."""
|
187
186
|
|
188
187
|
prompt_parts = [
|
@@ -192,23 +191,19 @@ replacing <file="…"> blocks as needed. Commentary goes outside those tags."""
|
|
192
191
|
"=== TASK ===",
|
193
192
|
task,
|
194
193
|
"",
|
195
|
-
"===
|
194
|
+
"=== GENERAL CONTEXT ===",
|
196
195
|
all_related_context,
|
196
|
+
"",
|
197
|
+
"=== CODE CONTEXT ===",
|
198
|
+
code_context,
|
197
199
|
]
|
198
200
|
|
199
|
-
if any_additional_context.strip():
|
200
|
-
prompt_parts.extend(
|
201
|
-
[
|
202
|
-
"",
|
203
|
-
"=== ADDITIONAL CONTEXT ===",
|
204
|
-
any_additional_context,
|
205
|
-
]
|
206
|
-
)
|
207
|
-
|
208
201
|
prompt_parts.extend(
|
209
202
|
[
|
210
203
|
"",
|
211
204
|
"=== INSTRUCTIONS ===",
|
205
|
+
"- Provide exhaustive, step-by-step reasoning.",
|
206
|
+
"- Never include files matching .gitignore patterns.",
|
212
207
|
"- Analyze the code and requirements step-by-step.",
|
213
208
|
"- Show your reasoning and propose concrete changes.",
|
214
209
|
'- Provide updated code using the XML format (<file_tree> plus <file="…"> blocks).',
|
@@ -0,0 +1 @@
|
|
1
|
+
# This file makes the `utils` directory a Python package.
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import glob
|
2
|
+
import os
|
3
|
+
|
4
|
+
import pathspec
|
5
|
+
|
6
|
+
|
7
|
+
def load_gitignore(base_dir: str) -> pathspec.PathSpec:
|
8
|
+
"""Loads .gitignore patterns from the specified base directory."""
|
9
|
+
gitignore_path = os.path.join(base_dir, ".gitignore")
|
10
|
+
patterns = []
|
11
|
+
if os.path.exists(gitignore_path):
|
12
|
+
with open(gitignore_path, encoding="utf-8") as f:
|
13
|
+
patterns = f.read().splitlines()
|
14
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
15
|
+
|
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
|
+
|
31
|
+
def filter_paths(paths: list[str], spec: pathspec.PathSpec, base_dir: str = ".") -> list[str]:
|
32
|
+
"""Filters out paths that match the .gitignore spec and non-text files."""
|
33
|
+
filtered_paths = []
|
34
|
+
for path in paths:
|
35
|
+
normalized_path = os.path.normpath(os.path.join(base_dir, path))
|
36
|
+
if not spec.match_file(normalized_path) and os.path.isfile(normalized_path):
|
37
|
+
try:
|
38
|
+
with open(normalized_path, encoding="utf-8") as f:
|
39
|
+
f.read(1024)
|
40
|
+
filtered_paths.append(path)
|
41
|
+
except (OSError, UnicodeDecodeError):
|
42
|
+
pass
|
43
|
+
return filtered_paths
|
44
|
+
|
45
|
+
|
46
|
+
def build_file_tree(paths: list[str], base_dir: str = ".") -> str:
|
47
|
+
"""Builds an ASCII file tree from a list of paths."""
|
48
|
+
tree = {}
|
49
|
+
for path in paths:
|
50
|
+
parts = path.split(os.sep)
|
51
|
+
current_level = tree
|
52
|
+
for part in parts:
|
53
|
+
if part not in current_level:
|
54
|
+
current_level[part] = {}
|
55
|
+
current_level = current_level[part]
|
56
|
+
|
57
|
+
def generate_tree_lines(d, prefix=""):
|
58
|
+
lines = []
|
59
|
+
entries = sorted(d.keys())
|
60
|
+
for i, entry in enumerate(entries):
|
61
|
+
connector = "├── " if i < len(entries) - 1 else "└── "
|
62
|
+
lines.append(prefix + connector + entry)
|
63
|
+
if d[entry]:
|
64
|
+
extension = "│ " if i < len(entries) - 1 else " "
|
65
|
+
lines.extend(generate_tree_lines(d[entry], prefix + extension))
|
66
|
+
return lines
|
67
|
+
|
68
|
+
tree_lines = [base_dir] + generate_tree_lines(tree)
|
69
|
+
return "\n".join(tree_lines)
|
70
|
+
|
71
|
+
|
72
|
+
def build_file_blocks(paths: list[str], base_dir: str = ".") -> str:
|
73
|
+
"""Creates <file="..." /> blocks with the full source code for each file."""
|
74
|
+
blocks = []
|
75
|
+
for path in paths:
|
76
|
+
full_path = os.path.join(base_dir, path)
|
77
|
+
try:
|
78
|
+
with open(full_path, encoding="utf-8") as f:
|
79
|
+
content = f.read()
|
80
|
+
blocks.append(f'<file="{path}">\n{content}\n</file>')
|
81
|
+
except (OSError, UnicodeDecodeError) as e:
|
82
|
+
blocks.append(f'<file="{path}">\nError reading file: {e}\n</file>')
|
83
|
+
return "\n\n".join(blocks)
|
84
|
+
|
85
|
+
|
86
|
+
def build_code_context(file_list: list[str], base_dir: str = ".") -> str:
|
87
|
+
"""
|
88
|
+
Builds the complete code context, including a file tree and file content blocks,
|
89
|
+
filtering out ignored and binary files.
|
90
|
+
"""
|
91
|
+
spec = load_gitignore(base_dir)
|
92
|
+
|
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.1.2.dist-info → phone_a_friend_mcp_server-0.3.0rc1.dist-info}/METADATA
RENAMED
@@ -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
|
@@ -17,6 +17,7 @@ Requires-Dist: aiofiles>=24.1.0
|
|
17
17
|
Requires-Dist: aiohttp>=3.12.7
|
18
18
|
Requires-Dist: click>=8.2.1
|
19
19
|
Requires-Dist: mcp>=1.9.2
|
20
|
+
Requires-Dist: pathspec>=0.12.1
|
20
21
|
Requires-Dist: pydantic-ai-slim[anthropic,google,openai]>=0.2.14
|
21
22
|
Requires-Dist: pydantic>=2.11.5
|
22
23
|
Requires-Dist: pyyaml>=6.0.0
|
@@ -46,7 +47,7 @@ This enables AI systems to leverage other AI models as "consultants" for complex
|
|
46
47
|
## Architecture 🏗️
|
47
48
|
|
48
49
|
```
|
49
|
-
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
|
50
51
|
```
|
51
52
|
|
52
53
|
**Sequential Workflow:**
|
@@ -63,87 +64,105 @@ Primary AI → Phone-a-Friend MCP → OpenRouter → External AI (GPT-4, Claude,
|
|
63
64
|
- Critical decision-making with high stakes
|
64
65
|
- Problems requiring multiple perspectives
|
65
66
|
|
66
|
-
##
|
67
|
+
## Quick Start ⚡
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
git clone https://github.com/abhishekbhakat/phone-a-friend-mcp-server.git
|
71
|
-
cd phone-a-friend-mcp-server
|
72
|
-
```
|
69
|
+
Configure your MCP client (e.g., Claude Desktop) using the JSON block below—no cloning or manual installation required.
|
70
|
+
The `uv` runner will automatically download and execute the server package if it isn't present.
|
73
71
|
|
74
|
-
|
75
|
-
|
76
|
-
|
72
|
+
Add the following JSON configuration to your MCP client and replace `<YOUR_API_KEY>` with your key:
|
73
|
+
|
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
|
+
]
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}
|
77
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.
|
78
89
|
|
79
|
-
|
90
|
+
## Available Tools 🛠️
|
80
91
|
|
81
|
-
|
82
|
-
|
83
|
-
export OPENROUTER_API_KEY="your-openrouter-key"
|
84
|
-
# OR
|
85
|
-
export OPENAI_API_KEY="your-openai-key"
|
86
|
-
# OR
|
87
|
-
export ANTHROPIC_API_KEY="your-anthropic-key"
|
88
|
-
# OR
|
89
|
-
export GOOGLE_API_KEY="your-google-key"
|
90
|
-
```
|
92
|
+
### phone_a_friend
|
93
|
+
📞 Consult external AI for critical thinking and complex reasoning. Makes API calls to get responses.
|
91
94
|
|
92
|
-
|
93
|
-
|
94
|
-
phone-a-friend-mcp-server --api-key "your-api-key" --provider openai
|
95
|
-
```
|
95
|
+
### fax_a_friend
|
96
|
+
📠 Generate master prompt file for manual AI consultation. Creates file for copy-paste workflow.
|
96
97
|
|
97
|
-
|
98
|
+
**Parameters**
|
98
99
|
|
99
|
-
|
100
|
-
```bash
|
100
|
+
*phone_a_friend*
|
101
101
|
|
102
|
-
|
103
|
-
|
102
|
+
- `all_related_context` (required): General, non-code context such as constraints, tracebacks, or high-level requirements.
|
103
|
+
- `file_list` (required): Array of file paths or glob patterns. **Just pass the paths** – the server automatically reads those files (skips anything in `.gitignore` or non-text/binary) and builds the full code context for the external AI.
|
104
|
+
- `task` (required): A clear, specific description of what you want the external AI to do.
|
104
105
|
|
105
|
-
|
106
|
-
phone-a-friend-mcp-server --temperature 0.4
|
106
|
+
*fax_a_friend*
|
107
107
|
|
108
|
-
|
109
|
-
|
110
|
-
|
108
|
+
- `all_related_context` (required): Same as above.
|
109
|
+
- `file_list` (required): Same as above.
|
110
|
+
- `task` (required): Same as above.
|
111
|
+
- `output_directory` (required): Directory where the generated `fax_a_friend.md` master prompt file will be saved.
|
111
112
|
|
112
|
-
|
113
|
-
```bash
|
113
|
+
## Advanced Configuration 🔧
|
114
114
|
|
115
|
-
|
116
|
-
export PHONE_A_FRIEND_MODEL="your-preferred-model"
|
117
|
-
export PHONE_A_FRIEND_PROVIDER="your-preferred-provider"
|
118
|
-
export PHONE_A_FRIEND_BASE_URL="https://custom-api.example.com"
|
115
|
+
This section covers all configuration options, including environment variables, CLI flags, and model selection.
|
119
116
|
|
120
|
-
|
121
|
-
|
117
|
+
### Providers and API Keys
|
118
|
+
|
119
|
+
The server can be configured via CLI flags or environment variables.
|
120
|
+
|
121
|
+
| Provider | CLI Flag | Environment Variable |
|
122
|
+
| :--- | :--- | :--- |
|
123
|
+
| OpenAI | `--provider openai` | `OPENAI_API_KEY` |
|
124
|
+
| OpenRouter | `--provider openrouter` | `OPENROUTER_API_KEY` |
|
125
|
+
| Anthropic | `--provider anthropic` | `ANTHROPIC_API_KEY` |
|
126
|
+
| Google | `--provider google` | `GOOGLE_API_KEY` |
|
127
|
+
|
128
|
+
**CLI Example:**
|
129
|
+
```bash
|
130
|
+
phone-a-friend-mcp-server --provider openai --api-key "sk-..."
|
122
131
|
```
|
123
132
|
|
124
|
-
|
133
|
+
**Environment Variable Example:**
|
134
|
+
```bash
|
135
|
+
export OPENAI_API_KEY="sk-..."
|
136
|
+
phone-a-friend-mcp-server
|
137
|
+
```
|
125
138
|
|
126
|
-
|
127
|
-
- **OpenAI**: o3
|
128
|
-
- **Anthropic**: Claude 4 Opus
|
129
|
-
- **Google**: Gemini 2.5 Pro Preview 05-06 (automatically set temperature to 0.0)
|
130
|
-
- **OpenRouter**: For other models like Deepseek or Qwen
|
139
|
+
### Model Selection
|
131
140
|
|
132
|
-
You can override the
|
141
|
+
You can override the default model for each provider.
|
133
142
|
|
134
|
-
|
143
|
+
| Provider | Default Model |
|
144
|
+
| :--- | :--- |
|
145
|
+
| **OpenAI** | `o3` |
|
146
|
+
| **Anthropic** | `Claude 4 Opus` |
|
147
|
+
| **Google** | `Gemini 2.5 Pro Preview 05-06` |
|
148
|
+
| **OpenRouter**| `anthropic/claude-4-opus` |
|
135
149
|
|
136
|
-
|
137
|
-
|
150
|
+
**Override with CLI:**
|
151
|
+
```bash
|
152
|
+
phone-a-friend-mcp-server --model "o3"
|
153
|
+
```
|
138
154
|
|
139
|
-
|
140
|
-
|
155
|
+
**Override with Environment Variable:**
|
156
|
+
```bash
|
157
|
+
export PHONE_A_FRIEND_MODEL="o3"
|
158
|
+
```
|
141
159
|
|
142
|
-
|
143
|
-
- `all_related_context` (required): All context related to the problem
|
144
|
-
- `any_additional_context` (optional): Additional helpful context
|
145
|
-
- `task` (required): Specific task or question for the AI
|
160
|
+
### Additional Options
|
146
161
|
|
162
|
+
| Feature | CLI Flag | Environment Variable | Default |
|
163
|
+
| :--- | :--- | :--- | :--- |
|
164
|
+
| **Temperature** | `--temperature 0.5` | `PHONE_A_FRIEND_TEMPERATURE` | `0.4` |
|
165
|
+
| **Base URL** | `--base-url ...` | `PHONE_A_FRIEND_BASE_URL` | Provider default |
|
147
166
|
|
148
167
|
## Use Cases 🎯
|
149
168
|
|
@@ -151,55 +170,6 @@ You can override the auto-selection by setting `PHONE_A_FRIEND_MODEL` environmen
|
|
151
170
|
2. For complex algorithms, data structures, or mathematical computations
|
152
171
|
3. Frontend Development with React, Vue, CSS, or modern frontend frameworks
|
153
172
|
|
154
|
-
## Claude Desktop Configuration 🖥️
|
155
|
-
|
156
|
-
To use Phone-a-Friend MCP server with Claude Desktop, add this configuration to your `claude_desktop_config.json` file:
|
157
|
-
|
158
|
-
### Configuration File Location
|
159
|
-
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
160
|
-
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
161
|
-
|
162
|
-
### Configuration
|
163
|
-
|
164
|
-
**Option 1: Using uv (Recommended)**
|
165
|
-
```json
|
166
|
-
{
|
167
|
-
"mcpServers": {
|
168
|
-
"phone-a-friend": {
|
169
|
-
"command": "uvx",
|
170
|
-
"args": [
|
171
|
-
"--refresh",
|
172
|
-
"phone-a-friend-mcp-server",
|
173
|
-
],
|
174
|
-
"env": {
|
175
|
-
"OPENROUTER_API_KEY": "your-openrouter-api-key",
|
176
|
-
"PHONE_A_FRIEND_MODEL": "anthropic/claude-4-opus",
|
177
|
-
"PHONE_A_FRIEND_TEMPERATURE": "0.4"
|
178
|
-
}
|
179
|
-
}
|
180
|
-
}
|
181
|
-
}
|
182
|
-
```
|
183
|
-
|
184
|
-
### Environment Variables in Configuration
|
185
|
-
|
186
|
-
You can configure different AI providers directly in the Claude Desktop config:
|
187
|
-
|
188
|
-
```json
|
189
|
-
{
|
190
|
-
"mcpServers": {
|
191
|
-
"phone-a-friend": {
|
192
|
-
"command": "phone-a-friend-mcp-server",
|
193
|
-
"env": {
|
194
|
-
"OPENROUTER_API_KEY": "your-openrouter-api-key",
|
195
|
-
"PHONE_A_FRIEND_MODEL": "anthropic/claude-4-opus",
|
196
|
-
"PHONE_A_FRIEND_TEMPERATURE": "0.4"
|
197
|
-
}
|
198
|
-
}
|
199
|
-
}
|
200
|
-
}
|
201
|
-
```
|
202
|
-
|
203
173
|
## License 📄
|
204
174
|
|
205
175
|
MIT License - see LICENSE file for details.
|
@@ -0,0 +1,17 @@
|
|
1
|
+
phone_a_friend_mcp_server/__init__.py,sha256=9sn_dPrIzLz4W7_Ww--o8aUxIhUI3YGNrxPa26pKShw,2025
|
2
|
+
phone_a_friend_mcp_server/__main__.py,sha256=A-8-jkY2FK2foabew5I-Wk2A54IwzWZcydlQKfiR-p4,51
|
3
|
+
phone_a_friend_mcp_server/config.py,sha256=AP8ZU5VEJjr73QUNAoPKidAhO2O_WOGO9mmj7-rTAz0,3988
|
4
|
+
phone_a_friend_mcp_server/server.py,sha256=z-O20j-j2oHFfFK8o0u9kn-MR8Q-Te0lRZOQfLkYUbM,3448
|
5
|
+
phone_a_friend_mcp_server/client/__init__.py,sha256=fsa8DXjz4rzYXmOUAdLdTpTwPSlZ3zobmBGXqnCEaWs,47
|
6
|
+
phone_a_friend_mcp_server/tools/__init__.py,sha256=jtuvmcStXzbaM8wuhOKC8M8mBqDjHr-ypZ2ct1Rgi7Q,46
|
7
|
+
phone_a_friend_mcp_server/tools/base_tools.py,sha256=DMjFq0E3TO9a9I7QY4wQ_B4-SntdXzSZzrYymFzSmVE,765
|
8
|
+
phone_a_friend_mcp_server/tools/fax_tool.py,sha256=muihVHXqLrP-H5TU8Ns2Civ_EOay_wuZO7y2Pcge5n8,8953
|
9
|
+
phone_a_friend_mcp_server/tools/phone_tool.py,sha256=X8MGkTxqAbo7RlaEYBdqGvfVVxhrbZ75RNnRwN_QK-s,8513
|
10
|
+
phone_a_friend_mcp_server/tools/tool_manager.py,sha256=VVtENC-n3D4GV6Cy3l9--30SJi06mJdyEiG7F_mfP7I,1474
|
11
|
+
phone_a_friend_mcp_server/utils/__init__.py,sha256=1IwFDtwJ76i1O7_iM4LLGqwgtt11y0PIV0DWubh8nLU,58
|
12
|
+
phone_a_friend_mcp_server/utils/context_builder.py,sha256=Fc1ej3disKnCrHbyYUInkHygOUr93izOyhFz3reP5gE,4580
|
13
|
+
phone_a_friend_mcp_server-0.3.0rc1.dist-info/METADATA,sha256=9i3VyjgZbDWTFOAhDocfSMIiAZCQG2Ec_uB_YCsTxts,6030
|
14
|
+
phone_a_friend_mcp_server-0.3.0rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
+
phone_a_friend_mcp_server-0.3.0rc1.dist-info/entry_points.txt,sha256=c_08XI-vG07VmUT3mtzyuCQjaus5l1NBl4q00Q3jLug,86
|
16
|
+
phone_a_friend_mcp_server-0.3.0rc1.dist-info/licenses/LICENSE,sha256=-8bInetillKZC0qZDT8RWYIOrph3HIU5cr5N4Pg7bBE,1065
|
17
|
+
phone_a_friend_mcp_server-0.3.0rc1.dist-info/RECORD,,
|
@@ -1,15 +0,0 @@
|
|
1
|
-
phone_a_friend_mcp_server/__init__.py,sha256=9sn_dPrIzLz4W7_Ww--o8aUxIhUI3YGNrxPa26pKShw,2025
|
2
|
-
phone_a_friend_mcp_server/__main__.py,sha256=A-8-jkY2FK2foabew5I-Wk2A54IwzWZcydlQKfiR-p4,51
|
3
|
-
phone_a_friend_mcp_server/config.py,sha256=McHqEzIVhSXpfmNLrCRlrFckRLzjxN5LESkqAXX6c4o,3953
|
4
|
-
phone_a_friend_mcp_server/server.py,sha256=z-O20j-j2oHFfFK8o0u9kn-MR8Q-Te0lRZOQfLkYUbM,3448
|
5
|
-
phone_a_friend_mcp_server/client/__init__.py,sha256=fsa8DXjz4rzYXmOUAdLdTpTwPSlZ3zobmBGXqnCEaWs,47
|
6
|
-
phone_a_friend_mcp_server/tools/__init__.py,sha256=jtuvmcStXzbaM8wuhOKC8M8mBqDjHr-ypZ2ct1Rgi7Q,46
|
7
|
-
phone_a_friend_mcp_server/tools/base_tools.py,sha256=DMjFq0E3TO9a9I7QY4wQ_B4-SntdXzSZzrYymFzSmVE,765
|
8
|
-
phone_a_friend_mcp_server/tools/fax_tool.py,sha256=qPKTDG1voXo_6MjgkMSDLN8AKYfal6sagK1l2BRPgCw,9399
|
9
|
-
phone_a_friend_mcp_server/tools/phone_tool.py,sha256=fPgYE8iRc9xFCQc07J5Zh4RPIbgYaiE6WygTEo6oUbs,8604
|
10
|
-
phone_a_friend_mcp_server/tools/tool_manager.py,sha256=VVtENC-n3D4GV6Cy3l9--30SJi06mJdyEiG7F_mfP7I,1474
|
11
|
-
phone_a_friend_mcp_server-0.1.2.dist-info/METADATA,sha256=sMh-qLXluzJMAR0YZcA-OaSajsxq4tAKODDNyG8Y_tc,6222
|
12
|
-
phone_a_friend_mcp_server-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
13
|
-
phone_a_friend_mcp_server-0.1.2.dist-info/entry_points.txt,sha256=c_08XI-vG07VmUT3mtzyuCQjaus5l1NBl4q00Q3jLug,86
|
14
|
-
phone_a_friend_mcp_server-0.1.2.dist-info/licenses/LICENSE,sha256=-8bInetillKZC0qZDT8RWYIOrph3HIU5cr5N4Pg7bBE,1065
|
15
|
-
phone_a_friend_mcp_server-0.1.2.dist-info/RECORD,,
|
{phone_a_friend_mcp_server-0.1.2.dist-info → phone_a_friend_mcp_server-0.3.0rc1.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|
File without changes
|