second-opinion-mcp 0.3.0__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.
- second_opinion_mcp/__init__.py +16 -0
- second_opinion_mcp/__main__.py +18 -0
- second_opinion_mcp/cli.py +272 -0
- second_opinion_mcp/server.py +1537 -0
- second_opinion_mcp-0.3.0.dist-info/METADATA +324 -0
- second_opinion_mcp-0.3.0.dist-info/RECORD +9 -0
- second_opinion_mcp-0.3.0.dist-info/WHEEL +4 -0
- second_opinion_mcp-0.3.0.dist-info/entry_points.txt +2 -0
- second_opinion_mcp-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Second Opinion MCP Server - Multi-model AI analysis for Claude."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.3.0"
|
|
4
|
+
__author__ = "MarvinFS"
|
|
5
|
+
|
|
6
|
+
from second_opinion_mcp.server import mcp, second_opinion, challenge, code_review, consensus, review_synthesis
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"__version__",
|
|
10
|
+
"mcp",
|
|
11
|
+
"second_opinion",
|
|
12
|
+
"challenge",
|
|
13
|
+
"code_review",
|
|
14
|
+
"consensus",
|
|
15
|
+
"review_synthesis",
|
|
16
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Entry point for running second-opinion-mcp as a module or CLI."""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
"""Main entry point supporting both server and setup commands."""
|
|
8
|
+
if len(sys.argv) > 1 and sys.argv[1] == "setup":
|
|
9
|
+
from second_opinion_mcp.cli import setup_wizard
|
|
10
|
+
sys.argv.pop(1) # Remove 'setup' from argv
|
|
11
|
+
setup_wizard()
|
|
12
|
+
else:
|
|
13
|
+
from second_opinion_mcp.server import main as server_main
|
|
14
|
+
server_main()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
main()
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Interactive setup wizard for second-opinion-mcp."""
|
|
3
|
+
import sys
|
|
4
|
+
import platform
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import questionary
|
|
8
|
+
from questionary import Style
|
|
9
|
+
except ImportError:
|
|
10
|
+
print("Error: questionary not installed. Run: pip install questionary")
|
|
11
|
+
sys.exit(1)
|
|
12
|
+
|
|
13
|
+
import keyring
|
|
14
|
+
|
|
15
|
+
# Provider configurations
|
|
16
|
+
PROVIDERS = {
|
|
17
|
+
"deepseek": {
|
|
18
|
+
"name": "DeepSeek V3.2",
|
|
19
|
+
"description": "DeepSeek Reasoner with chain-of-thought reasoning",
|
|
20
|
+
"keyring_service": "second-opinion",
|
|
21
|
+
"keyring_name": "deepseek",
|
|
22
|
+
"key_hint": "Get your key at https://platform.deepseek.com",
|
|
23
|
+
"key_prefix": "sk-",
|
|
24
|
+
},
|
|
25
|
+
"moonshot": {
|
|
26
|
+
"name": "Kimi K2.5 (Moonshot)",
|
|
27
|
+
"description": "Moonshot AI's Kimi with thinking mode",
|
|
28
|
+
"keyring_service": "second-opinion",
|
|
29
|
+
"keyring_name": "moonshot",
|
|
30
|
+
"key_hint": "Get your key at https://platform.moonshot.cn",
|
|
31
|
+
"key_prefix": None,
|
|
32
|
+
},
|
|
33
|
+
"openrouter": {
|
|
34
|
+
"name": "OpenRouter",
|
|
35
|
+
"description": "Access 300+ models via unified API",
|
|
36
|
+
"keyring_service": "second-opinion",
|
|
37
|
+
"keyring_name": "openrouter",
|
|
38
|
+
"key_hint": "Get your key at https://openrouter.ai/keys",
|
|
39
|
+
"key_prefix": "sk-or-",
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Custom style for questionary
|
|
44
|
+
custom_style = Style([
|
|
45
|
+
("qmark", "fg:cyan bold"),
|
|
46
|
+
("question", "bold"),
|
|
47
|
+
("answer", "fg:cyan"),
|
|
48
|
+
("pointer", "fg:cyan bold"),
|
|
49
|
+
("highlighted", "fg:cyan bold"),
|
|
50
|
+
("selected", "fg:green"),
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def check_existing_keys() -> dict[str, bool]:
|
|
55
|
+
"""Check which providers already have API keys configured."""
|
|
56
|
+
existing = {}
|
|
57
|
+
for provider, config in PROVIDERS.items():
|
|
58
|
+
try:
|
|
59
|
+
key = keyring.get_password(config["keyring_service"], config["keyring_name"])
|
|
60
|
+
existing[provider] = bool(key)
|
|
61
|
+
except Exception:
|
|
62
|
+
existing[provider] = False
|
|
63
|
+
return existing
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def store_api_key(provider: str, key: str) -> bool:
|
|
67
|
+
"""Store API key in system keyring."""
|
|
68
|
+
config = PROVIDERS[provider]
|
|
69
|
+
try:
|
|
70
|
+
keyring.set_password(config["keyring_service"], config["keyring_name"], key)
|
|
71
|
+
return True
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(f"Error storing key: {e}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def validate_api_key(provider: str, key: str) -> tuple[bool, str]:
|
|
78
|
+
"""Basic validation of API key format."""
|
|
79
|
+
if not key or not key.strip():
|
|
80
|
+
return False, "API key cannot be empty"
|
|
81
|
+
|
|
82
|
+
config = PROVIDERS[provider]
|
|
83
|
+
key = key.strip()
|
|
84
|
+
|
|
85
|
+
if config["key_prefix"] and not key.startswith(config["key_prefix"]):
|
|
86
|
+
return False, f"Key should start with '{config['key_prefix']}'"
|
|
87
|
+
|
|
88
|
+
if len(key) < 20:
|
|
89
|
+
return False, "Key seems too short"
|
|
90
|
+
|
|
91
|
+
return True, ""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_registration_command(selected_providers: list[str]) -> str:
|
|
95
|
+
"""Generate the registration command for Claude Code CLI."""
|
|
96
|
+
providers_env = ",".join(selected_providers)
|
|
97
|
+
|
|
98
|
+
if platform.system() == "Windows":
|
|
99
|
+
return f'''claude mcp add -s user second-opinion -e SECOND_OPINION_PROVIDERS="{providers_env}" -- second-opinion-mcp'''
|
|
100
|
+
else:
|
|
101
|
+
return f'''claude mcp add -s user second-opinion -e SECOND_OPINION_PROVIDERS="{providers_env}" -- second-opinion-mcp'''
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_desktop_config(selected_providers: list[str]) -> str:
|
|
105
|
+
"""Generate Claude Desktop configuration JSON."""
|
|
106
|
+
providers_env = ",".join(selected_providers)
|
|
107
|
+
|
|
108
|
+
return f'''{{
|
|
109
|
+
"mcpServers": {{
|
|
110
|
+
"second-opinion": {{
|
|
111
|
+
"command": "second-opinion-mcp",
|
|
112
|
+
"env": {{
|
|
113
|
+
"SECOND_OPINION_PROVIDERS": "{providers_env}"
|
|
114
|
+
}}
|
|
115
|
+
}}
|
|
116
|
+
}}
|
|
117
|
+
}}'''
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def setup_wizard():
|
|
121
|
+
"""Interactive setup wizard for second-opinion-mcp."""
|
|
122
|
+
print()
|
|
123
|
+
print("=" * 60)
|
|
124
|
+
print(" Second Opinion MCP - Setup Wizard")
|
|
125
|
+
print("=" * 60)
|
|
126
|
+
print()
|
|
127
|
+
print("This wizard will help you configure API keys for AI providers.")
|
|
128
|
+
print("Keys are stored securely in your system keyring.")
|
|
129
|
+
print()
|
|
130
|
+
|
|
131
|
+
# Check existing configuration
|
|
132
|
+
existing = check_existing_keys()
|
|
133
|
+
has_existing = any(existing.values())
|
|
134
|
+
|
|
135
|
+
if has_existing:
|
|
136
|
+
configured = [p for p, has_key in existing.items() if has_key]
|
|
137
|
+
print(f"Already configured: {', '.join(configured)}")
|
|
138
|
+
print()
|
|
139
|
+
|
|
140
|
+
# Provider selection
|
|
141
|
+
choices = []
|
|
142
|
+
for provider, config in PROVIDERS.items():
|
|
143
|
+
label = config["name"]
|
|
144
|
+
if existing.get(provider):
|
|
145
|
+
label += " (configured)"
|
|
146
|
+
choices.append(questionary.Choice(label, value=provider, checked=existing.get(provider, False)))
|
|
147
|
+
|
|
148
|
+
selected = questionary.checkbox(
|
|
149
|
+
"Select providers to configure (at least 2 recommended):",
|
|
150
|
+
choices=choices,
|
|
151
|
+
style=custom_style,
|
|
152
|
+
validate=lambda x: len(x) >= 1 or "Select at least one provider",
|
|
153
|
+
).ask()
|
|
154
|
+
|
|
155
|
+
if not selected:
|
|
156
|
+
print("Setup cancelled.")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
if len(selected) < 2:
|
|
160
|
+
print()
|
|
161
|
+
print("Note: Using 2+ providers enables the 'consensus' tool for multi-model debates.")
|
|
162
|
+
proceed = questionary.confirm(
|
|
163
|
+
"Continue with single provider?",
|
|
164
|
+
default=True,
|
|
165
|
+
style=custom_style,
|
|
166
|
+
).ask()
|
|
167
|
+
if not proceed:
|
|
168
|
+
return setup_wizard() # Restart
|
|
169
|
+
|
|
170
|
+
print()
|
|
171
|
+
|
|
172
|
+
# Configure API keys
|
|
173
|
+
for provider in selected:
|
|
174
|
+
config = PROVIDERS[provider]
|
|
175
|
+
|
|
176
|
+
if existing.get(provider):
|
|
177
|
+
update = questionary.confirm(
|
|
178
|
+
f"{config['name']} already configured. Update API key?",
|
|
179
|
+
default=False,
|
|
180
|
+
style=custom_style,
|
|
181
|
+
).ask()
|
|
182
|
+
if not update:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
print(f"\n{config['name']}")
|
|
186
|
+
print(f" {config['key_hint']}")
|
|
187
|
+
|
|
188
|
+
while True:
|
|
189
|
+
key = questionary.password(
|
|
190
|
+
f" Enter API key:",
|
|
191
|
+
style=custom_style,
|
|
192
|
+
).ask()
|
|
193
|
+
|
|
194
|
+
if key is None: # User cancelled
|
|
195
|
+
print(" Skipped.")
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
valid, error = validate_api_key(provider, key)
|
|
199
|
+
if not valid:
|
|
200
|
+
print(f" {error}")
|
|
201
|
+
retry = questionary.confirm("Try again?", default=True, style=custom_style).ask()
|
|
202
|
+
if not retry:
|
|
203
|
+
break
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
if store_api_key(provider, key.strip()):
|
|
207
|
+
print(" Key stored successfully.")
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
# Generate registration commands
|
|
211
|
+
print()
|
|
212
|
+
print("=" * 60)
|
|
213
|
+
print(" Setup Complete!")
|
|
214
|
+
print("=" * 60)
|
|
215
|
+
print()
|
|
216
|
+
|
|
217
|
+
# Filter to providers that now have keys
|
|
218
|
+
configured = []
|
|
219
|
+
for provider in selected:
|
|
220
|
+
try:
|
|
221
|
+
key = keyring.get_password(
|
|
222
|
+
PROVIDERS[provider]["keyring_service"],
|
|
223
|
+
PROVIDERS[provider]["keyring_name"]
|
|
224
|
+
)
|
|
225
|
+
if key:
|
|
226
|
+
configured.append(provider)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
if not configured:
|
|
231
|
+
print("No API keys were configured. Run 'second-opinion-mcp setup' to try again.")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
print(f"Configured providers: {', '.join(configured)}")
|
|
235
|
+
print()
|
|
236
|
+
|
|
237
|
+
# Show registration command
|
|
238
|
+
print("For Claude Code CLI, run:")
|
|
239
|
+
print()
|
|
240
|
+
print(f" {get_registration_command(configured)}")
|
|
241
|
+
print()
|
|
242
|
+
|
|
243
|
+
# Show desktop config
|
|
244
|
+
show_desktop = questionary.confirm(
|
|
245
|
+
"Show Claude Desktop configuration?",
|
|
246
|
+
default=False,
|
|
247
|
+
style=custom_style,
|
|
248
|
+
).ask()
|
|
249
|
+
|
|
250
|
+
if show_desktop:
|
|
251
|
+
print()
|
|
252
|
+
print("Add this to your claude_desktop_config.json:")
|
|
253
|
+
print()
|
|
254
|
+
print(get_desktop_config(configured))
|
|
255
|
+
print()
|
|
256
|
+
if platform.system() == "Windows":
|
|
257
|
+
print("Location: %APPDATA%\\Claude\\claude_desktop_config.json")
|
|
258
|
+
elif platform.system() == "Darwin":
|
|
259
|
+
print("Location: ~/Library/Application Support/Claude/claude_desktop_config.json")
|
|
260
|
+
else:
|
|
261
|
+
print("Location: ~/.config/Claude/claude_desktop_config.json")
|
|
262
|
+
|
|
263
|
+
print()
|
|
264
|
+
print("Test the server with:")
|
|
265
|
+
print(" second-opinion-mcp")
|
|
266
|
+
print()
|
|
267
|
+
print("The server will start and wait for input. Press Ctrl+C to stop.")
|
|
268
|
+
print()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
setup_wizard()
|