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.
@@ -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()