second-opinion-mcp 0.3.4__py3-none-any.whl → 0.3.5__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 +1 -1
- second_opinion_mcp/cli.py +341 -64
- {second_opinion_mcp-0.3.4.dist-info → second_opinion_mcp-0.3.5.dist-info}/METADATA +34 -13
- second_opinion_mcp-0.3.5.dist-info/RECORD +9 -0
- second_opinion_mcp-0.3.4.dist-info/RECORD +0 -9
- {second_opinion_mcp-0.3.4.dist-info → second_opinion_mcp-0.3.5.dist-info}/WHEEL +0 -0
- {second_opinion_mcp-0.3.4.dist-info → second_opinion_mcp-0.3.5.dist-info}/entry_points.txt +0 -0
- {second_opinion_mcp-0.3.4.dist-info → second_opinion_mcp-0.3.5.dist-info}/licenses/LICENSE +0 -0
second_opinion_mcp/__init__.py
CHANGED
second_opinion_mcp/cli.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""Interactive setup wizard for second-opinion-mcp."""
|
|
3
3
|
import sys
|
|
4
4
|
import platform
|
|
5
|
+
import os
|
|
5
6
|
|
|
6
7
|
try:
|
|
7
8
|
import questionary
|
|
@@ -52,6 +53,50 @@ custom_style = Style([
|
|
|
52
53
|
])
|
|
53
54
|
|
|
54
55
|
|
|
56
|
+
def detect_installation_method() -> tuple[str, str | None]:
|
|
57
|
+
"""Detect how second-opinion-mcp was installed.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
tuple: (method, python_path) where method is 'pipx', 'pip', or 'unknown'
|
|
61
|
+
and python_path is the path to Python executable if found.
|
|
62
|
+
"""
|
|
63
|
+
home = os.path.expanduser("~")
|
|
64
|
+
system = platform.system()
|
|
65
|
+
|
|
66
|
+
# Check for pipx installation
|
|
67
|
+
if system == "Windows":
|
|
68
|
+
pipx_paths = [
|
|
69
|
+
os.path.join(home, ".local", "pipx", "venvs", "second-opinion-mcp", "Scripts", "python.exe"),
|
|
70
|
+
os.path.join(home, "pipx", "venvs", "second-opinion-mcp", "Scripts", "python.exe"),
|
|
71
|
+
os.path.join(os.environ.get("LOCALAPPDATA", ""), "pipx", "venvs", "second-opinion-mcp", "Scripts", "python.exe"),
|
|
72
|
+
]
|
|
73
|
+
else:
|
|
74
|
+
pipx_paths = [
|
|
75
|
+
os.path.join(home, ".local", "share", "pipx", "venvs", "second-opinion-mcp", "bin", "python3"),
|
|
76
|
+
os.path.join(home, ".local", "share", "pipx", "venvs", "second-opinion-mcp", "bin", "python"),
|
|
77
|
+
os.path.join(home, ".local", "pipx", "venvs", "second-opinion-mcp", "bin", "python3"),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
for path in pipx_paths:
|
|
81
|
+
if os.path.exists(path):
|
|
82
|
+
return "pipx", path
|
|
83
|
+
|
|
84
|
+
# Check if we're running from a venv (pip install -e . or pip install)
|
|
85
|
+
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
|
|
86
|
+
return "pip", sys.executable
|
|
87
|
+
|
|
88
|
+
# Unknown installation method
|
|
89
|
+
return "unknown", sys.executable
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_pipx_python_path() -> str | None:
|
|
93
|
+
"""Get the Python path from pipx venv if installed via pipx."""
|
|
94
|
+
method, path = detect_installation_method()
|
|
95
|
+
if method == "pipx":
|
|
96
|
+
return path
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
55
100
|
def check_keyring_available() -> tuple[bool, str]:
|
|
56
101
|
"""Check if keyring backend is available and working."""
|
|
57
102
|
try:
|
|
@@ -94,29 +139,43 @@ def print_keyring_setup_instructions():
|
|
|
94
139
|
print()
|
|
95
140
|
|
|
96
141
|
system = platform.system()
|
|
142
|
+
install_method, _ = detect_installation_method()
|
|
97
143
|
|
|
98
144
|
if system == "Linux":
|
|
99
|
-
|
|
145
|
+
if install_method == "pipx":
|
|
146
|
+
print("For Linux (pipx installation detected), run:")
|
|
147
|
+
print()
|
|
148
|
+
print(" # Install the encrypted file backend into the pipx environment")
|
|
149
|
+
print(" pipx inject second-opinion-mcp keyrings.alt")
|
|
150
|
+
else:
|
|
151
|
+
print("For Linux, install the encrypted file backend:")
|
|
152
|
+
print()
|
|
153
|
+
print(" pip install keyrings.alt")
|
|
100
154
|
print()
|
|
101
|
-
print("
|
|
102
|
-
print("
|
|
103
|
-
print("
|
|
155
|
+
print(" # Create the keyring config")
|
|
156
|
+
print(" mkdir -p ~/.local/share/python_keyring")
|
|
157
|
+
print(" cat > ~/.local/share/python_keyring/keyringrc.cfg << 'EOF'")
|
|
158
|
+
print(" [backend]")
|
|
159
|
+
print(" default-keyring=keyrings.alt.file.EncryptedKeyring")
|
|
160
|
+
print(" EOF")
|
|
104
161
|
print()
|
|
105
|
-
print("
|
|
106
|
-
print("
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
print(" EOF")
|
|
162
|
+
print("Alternative for desktop Linux (GNOME/KDE):")
|
|
163
|
+
print(" sudo apt install gnome-keyring # or kde-wallet")
|
|
164
|
+
if install_method == "pipx":
|
|
165
|
+
print(" pipx inject second-opinion-mcp secretstorage")
|
|
166
|
+
else:
|
|
167
|
+
print(" pip install secretstorage")
|
|
112
168
|
print()
|
|
113
|
-
print("
|
|
169
|
+
print("Then run 'second-opinion-mcp setup' again.")
|
|
114
170
|
|
|
115
171
|
elif system == "Darwin":
|
|
116
172
|
print("For macOS, keyring should work automatically with Keychain.")
|
|
117
173
|
print("If you see this error, try:")
|
|
118
174
|
print()
|
|
119
|
-
|
|
175
|
+
if install_method == "pipx":
|
|
176
|
+
print(" pipx reinstall second-opinion-mcp")
|
|
177
|
+
else:
|
|
178
|
+
print(" pip install --upgrade keyring")
|
|
120
179
|
print()
|
|
121
180
|
print("Then run 'second-opinion-mcp setup' again.")
|
|
122
181
|
|
|
@@ -124,39 +183,95 @@ def print_keyring_setup_instructions():
|
|
|
124
183
|
print("For Windows, keyring should work automatically with Credential Manager.")
|
|
125
184
|
print("If you see this error, try:")
|
|
126
185
|
print()
|
|
127
|
-
|
|
186
|
+
if install_method == "pipx":
|
|
187
|
+
print(" pipx reinstall second-opinion-mcp")
|
|
188
|
+
else:
|
|
189
|
+
print(" pip install --upgrade keyring")
|
|
128
190
|
print()
|
|
129
191
|
print("Then run 'second-opinion-mcp setup' again.")
|
|
130
192
|
|
|
131
193
|
print()
|
|
132
194
|
|
|
133
195
|
|
|
134
|
-
def print_manual_setup_instructions(providers: list[str] | None = None):
|
|
196
|
+
def print_manual_setup_instructions(providers: list[str] | None = None, existing: dict[str, bool] | None = None):
|
|
135
197
|
"""Print instructions for manual keyring setup."""
|
|
136
198
|
print()
|
|
137
199
|
print("=" * 60)
|
|
138
200
|
print(" Manual API Key Setup")
|
|
139
201
|
print("=" * 60)
|
|
140
202
|
print()
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
203
|
+
|
|
204
|
+
# Detect installation method
|
|
205
|
+
install_method, python_path = detect_installation_method()
|
|
206
|
+
system = platform.system()
|
|
207
|
+
|
|
208
|
+
if system == "Linux":
|
|
209
|
+
print("Step 1: Install keyrings.alt backend (if not done):")
|
|
210
|
+
print()
|
|
211
|
+
if install_method == "pipx":
|
|
212
|
+
print(" pipx inject second-opinion-mcp keyrings.alt")
|
|
213
|
+
else:
|
|
214
|
+
print(" pip install keyrings.alt")
|
|
215
|
+
print(" mkdir -p ~/.local/share/python_keyring")
|
|
216
|
+
print(" cat > ~/.local/share/python_keyring/keyringrc.cfg << 'EOF'")
|
|
217
|
+
print(" [backend]")
|
|
218
|
+
print(" default-keyring=keyrings.alt.file.EncryptedKeyring")
|
|
219
|
+
print(" EOF")
|
|
220
|
+
print()
|
|
221
|
+
print("Step 2: Store API keys (at least 2 required):")
|
|
222
|
+
print()
|
|
223
|
+
else:
|
|
224
|
+
print("IMPORTANT: You must configure at least 2 providers for the")
|
|
225
|
+
print("'consensus' tool to work. Single-provider mode is limited.")
|
|
226
|
+
print()
|
|
227
|
+
print("Run these commands to store your API keys:")
|
|
228
|
+
print()
|
|
229
|
+
|
|
230
|
+
# Determine which Python to use based on installation method
|
|
231
|
+
if install_method == "pipx" and python_path:
|
|
232
|
+
python_cmd = python_path
|
|
233
|
+
print(f" # Using pipx environment: {python_path}")
|
|
234
|
+
print()
|
|
235
|
+
elif python_path:
|
|
236
|
+
python_cmd = python_path
|
|
237
|
+
print(f" # Using Python: {python_path}")
|
|
238
|
+
print()
|
|
239
|
+
else:
|
|
240
|
+
python_cmd = "python3"
|
|
241
|
+
print(" # Note: If 'import keyring' fails, ensure keyring is installed:")
|
|
242
|
+
if install_method == "pipx":
|
|
243
|
+
print(" # pipx inject second-opinion-mcp keyring")
|
|
244
|
+
else:
|
|
245
|
+
print(" # pip install keyring")
|
|
246
|
+
print()
|
|
146
247
|
|
|
147
248
|
target_providers = providers if providers else list(PROVIDERS.keys())
|
|
148
249
|
|
|
250
|
+
# Show status if we have existing info
|
|
251
|
+
if existing:
|
|
252
|
+
configured = [p for p in target_providers if existing.get(p)]
|
|
253
|
+
not_configured = [p for p in target_providers if not existing.get(p)]
|
|
254
|
+
if configured:
|
|
255
|
+
print(f" Already configured: {', '.join(configured)}")
|
|
256
|
+
if not_configured:
|
|
257
|
+
print(f" Need to configure: {', '.join(not_configured)}")
|
|
258
|
+
print()
|
|
259
|
+
|
|
149
260
|
for provider in target_providers:
|
|
150
261
|
config = PROVIDERS[provider]
|
|
151
|
-
|
|
152
|
-
|
|
262
|
+
status = ""
|
|
263
|
+
if existing and existing.get(provider):
|
|
264
|
+
status = " (already configured)"
|
|
265
|
+
print(f" # {config['name']}{status} - Get key at {config['key_hint']}")
|
|
266
|
+
print(f" {python_cmd} -c \"import keyring; keyring.set_password('{config['keyring_service']}', '{config['keyring_name']}', 'YOUR_API_KEY')\"")
|
|
153
267
|
print()
|
|
154
268
|
|
|
155
269
|
print("To verify your keys are stored:")
|
|
156
270
|
print()
|
|
157
|
-
|
|
271
|
+
verify_providers = "', '".join([f"{p}:", "bool(keyring.get_password('second-opinion', '{p}'))".replace("{p}", p) for p in ["deepseek", "moonshot", "openrouter"]])
|
|
272
|
+
print(f" {python_cmd} -c \"import keyring; print('deepseek:', bool(keyring.get_password('second-opinion', 'deepseek'))); print('moonshot:', bool(keyring.get_password('second-opinion', 'moonshot'))); print('openrouter:', bool(keyring.get_password('second-opinion', 'openrouter')))\"")
|
|
158
273
|
print()
|
|
159
|
-
print("After configuring keys, register with Claude Code:")
|
|
274
|
+
print("After configuring at least 2 keys, register with Claude Code:")
|
|
160
275
|
print()
|
|
161
276
|
|
|
162
277
|
if providers and len(providers) >= 1:
|
|
@@ -234,6 +349,47 @@ def get_desktop_config(selected_providers: list[str]) -> str:
|
|
|
234
349
|
}}'''
|
|
235
350
|
|
|
236
351
|
|
|
352
|
+
def print_client_instructions(configured_providers: list[str]):
|
|
353
|
+
"""Print setup instructions for all supported MCP clients."""
|
|
354
|
+
providers_env = ",".join(configured_providers)
|
|
355
|
+
|
|
356
|
+
print()
|
|
357
|
+
print("=" * 60)
|
|
358
|
+
print(" Client Setup Instructions")
|
|
359
|
+
print("=" * 60)
|
|
360
|
+
|
|
361
|
+
# Claude Code CLI
|
|
362
|
+
print()
|
|
363
|
+
print("CLAUDE CODE CLI:")
|
|
364
|
+
print(f" claude mcp add -s user second-opinion -e SECOND_OPINION_PROVIDERS=\"{providers_env}\" -- second-opinion-mcp")
|
|
365
|
+
|
|
366
|
+
# Claude Desktop
|
|
367
|
+
print()
|
|
368
|
+
print("CLAUDE DESKTOP APP:")
|
|
369
|
+
print(" Add to claude_desktop_config.json:")
|
|
370
|
+
if platform.system() == "Windows":
|
|
371
|
+
print(" Location: %APPDATA%\\Claude\\claude_desktop_config.json")
|
|
372
|
+
elif platform.system() == "Darwin":
|
|
373
|
+
print(" Location: ~/Library/Application Support/Claude/claude_desktop_config.json")
|
|
374
|
+
else:
|
|
375
|
+
print(" Location: ~/.config/Claude/claude_desktop_config.json")
|
|
376
|
+
print()
|
|
377
|
+
print(get_desktop_config(configured_providers))
|
|
378
|
+
|
|
379
|
+
# OpenClaw / Other MCP clients
|
|
380
|
+
print()
|
|
381
|
+
print("OPENCLAW / OTHER MCP CLIENTS:")
|
|
382
|
+
print(" Server command: second-opinion-mcp")
|
|
383
|
+
print(f" Environment: SECOND_OPINION_PROVIDERS={providers_env}")
|
|
384
|
+
print()
|
|
385
|
+
print(" For OpenClaw, add in settings with:")
|
|
386
|
+
print(" - Name: second-opinion")
|
|
387
|
+
print(" - Command: second-opinion-mcp")
|
|
388
|
+
print(f" - Environment variables: SECOND_OPINION_PROVIDERS={providers_env}")
|
|
389
|
+
|
|
390
|
+
print()
|
|
391
|
+
|
|
392
|
+
|
|
237
393
|
def setup_wizard():
|
|
238
394
|
"""Interactive setup wizard for second-opinion-mcp."""
|
|
239
395
|
print()
|
|
@@ -272,12 +428,89 @@ def setup_wizard():
|
|
|
272
428
|
# Check existing configuration
|
|
273
429
|
existing = check_existing_keys()
|
|
274
430
|
configured_count = sum(1 for v in existing.values() if v)
|
|
431
|
+
configured = [p for p, has_key in existing.items() if has_key]
|
|
432
|
+
not_configured = [p for p, has_key in existing.items() if not has_key]
|
|
275
433
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if
|
|
280
|
-
|
|
434
|
+
# Show current status
|
|
435
|
+
print("Current status:")
|
|
436
|
+
for provider, config in PROVIDERS.items():
|
|
437
|
+
status = "configured" if existing.get(provider) else "not configured"
|
|
438
|
+
print(f" {config['name']}: {status}")
|
|
439
|
+
print()
|
|
440
|
+
|
|
441
|
+
# If 2+ providers already configured, offer quick path
|
|
442
|
+
if configured_count >= 2:
|
|
443
|
+
print(f"You have {configured_count} providers configured (minimum 2 required).")
|
|
444
|
+
print()
|
|
445
|
+
|
|
446
|
+
action = questionary.select(
|
|
447
|
+
"What would you like to do?",
|
|
448
|
+
choices=[
|
|
449
|
+
questionary.Choice("Show registration command (ready to use)", value="register"),
|
|
450
|
+
questionary.Choice("Add or update API keys", value="configure"),
|
|
451
|
+
questionary.Choice("Manual setup (show CLI commands)", value="manual"),
|
|
452
|
+
],
|
|
453
|
+
style=custom_style,
|
|
454
|
+
).ask()
|
|
455
|
+
|
|
456
|
+
if action == "register":
|
|
457
|
+
client_choice = questionary.select(
|
|
458
|
+
"Which client will you use?",
|
|
459
|
+
choices=[
|
|
460
|
+
questionary.Choice("Claude Code CLI (Recommended)", value="claude-code"),
|
|
461
|
+
questionary.Choice("Claude Desktop App", value="claude-desktop"),
|
|
462
|
+
questionary.Choice("OpenClaw / Other MCP Client", value="other"),
|
|
463
|
+
questionary.Choice("Show all client instructions", value="all"),
|
|
464
|
+
],
|
|
465
|
+
style=custom_style,
|
|
466
|
+
).ask()
|
|
467
|
+
|
|
468
|
+
if client_choice == "all":
|
|
469
|
+
print_client_instructions(configured)
|
|
470
|
+
elif client_choice == "claude-code":
|
|
471
|
+
print()
|
|
472
|
+
print("Register with Claude Code CLI:")
|
|
473
|
+
print()
|
|
474
|
+
print(f" {get_registration_command(configured)}")
|
|
475
|
+
print()
|
|
476
|
+
print("Then restart Claude Code.")
|
|
477
|
+
elif client_choice == "claude-desktop":
|
|
478
|
+
print()
|
|
479
|
+
print("Add this to your claude_desktop_config.json:")
|
|
480
|
+
print()
|
|
481
|
+
print(get_desktop_config(configured))
|
|
482
|
+
print()
|
|
483
|
+
if platform.system() == "Windows":
|
|
484
|
+
print("Location: %APPDATA%\\Claude\\claude_desktop_config.json")
|
|
485
|
+
elif platform.system() == "Darwin":
|
|
486
|
+
print("Location: ~/Library/Application Support/Claude/claude_desktop_config.json")
|
|
487
|
+
else:
|
|
488
|
+
print("Location: ~/.config/Claude/claude_desktop_config.json")
|
|
489
|
+
print()
|
|
490
|
+
print("Then restart Claude Desktop.")
|
|
491
|
+
else:
|
|
492
|
+
providers_env = ",".join(configured)
|
|
493
|
+
print()
|
|
494
|
+
print("For OpenClaw or other MCP clients:")
|
|
495
|
+
print()
|
|
496
|
+
print(" Server command: second-opinion-mcp")
|
|
497
|
+
print(f" Environment: SECOND_OPINION_PROVIDERS={providers_env}")
|
|
498
|
+
print()
|
|
499
|
+
print(" Add the server in your client's MCP settings with these values.")
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
elif action == "manual":
|
|
503
|
+
print_manual_setup_instructions(list(PROVIDERS.keys()), existing)
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
# Fall through to configure flow
|
|
507
|
+
|
|
508
|
+
elif configured_count == 1:
|
|
509
|
+
print(f"Only {configured_count} provider configured. You need at least 2.")
|
|
510
|
+
print("The 'consensus' tool requires multiple models to debate.")
|
|
511
|
+
print()
|
|
512
|
+
else:
|
|
513
|
+
print("No providers configured yet. You need at least 2.")
|
|
281
514
|
print()
|
|
282
515
|
|
|
283
516
|
# Setup method selection
|
|
@@ -314,33 +547,55 @@ def setup_wizard():
|
|
|
314
547
|
if not selected:
|
|
315
548
|
selected = list(PROVIDERS.keys())
|
|
316
549
|
|
|
317
|
-
print_manual_setup_instructions(selected)
|
|
550
|
+
print_manual_setup_instructions(selected, existing)
|
|
318
551
|
return
|
|
319
552
|
|
|
320
|
-
# Interactive setup
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
label
|
|
326
|
-
|
|
553
|
+
# Interactive setup - determine which providers to configure
|
|
554
|
+
if not_configured:
|
|
555
|
+
# Default: configure unconfigured providers
|
|
556
|
+
choices = []
|
|
557
|
+
for provider, config in PROVIDERS.items():
|
|
558
|
+
label = config["name"]
|
|
559
|
+
if existing.get(provider):
|
|
560
|
+
label += " (configured)"
|
|
561
|
+
# Pre-check unconfigured ones, or all if less than 2 configured
|
|
562
|
+
should_check = not existing.get(provider)
|
|
563
|
+
choices.append(questionary.Choice(label, value=provider, checked=should_check))
|
|
564
|
+
|
|
565
|
+
# Calculate how many more we need
|
|
566
|
+
needed = max(0, 2 - configured_count)
|
|
327
567
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
568
|
+
selected = questionary.checkbox(
|
|
569
|
+
f"Select providers to configure (need {needed} more for minimum 2):",
|
|
570
|
+
choices=choices,
|
|
571
|
+
style=custom_style,
|
|
572
|
+
validate=lambda x: len(x) >= 1 or "Select at least one provider",
|
|
573
|
+
).ask()
|
|
574
|
+
else:
|
|
575
|
+
# All configured, but user wants to update
|
|
576
|
+
choices = [
|
|
577
|
+
questionary.Choice(f"{config['name']} (configured)", value=provider)
|
|
578
|
+
for provider, config in PROVIDERS.items()
|
|
579
|
+
]
|
|
580
|
+
|
|
581
|
+
selected = questionary.checkbox(
|
|
582
|
+
"Select providers to update:",
|
|
583
|
+
choices=choices,
|
|
584
|
+
style=custom_style,
|
|
585
|
+
).ask()
|
|
334
586
|
|
|
335
587
|
if not selected:
|
|
336
588
|
print("Setup cancelled.")
|
|
337
589
|
return
|
|
338
590
|
|
|
339
|
-
#
|
|
340
|
-
|
|
341
|
-
|
|
591
|
+
# Check if this will meet minimum requirement
|
|
592
|
+
will_configure_new = len([p for p in selected if not existing.get(p)])
|
|
593
|
+
total_after = configured_count + will_configure_new
|
|
594
|
+
|
|
595
|
+
if total_after < 2 and will_configure_new > 0:
|
|
342
596
|
print()
|
|
343
|
-
print("WARNING:
|
|
597
|
+
print(f"WARNING: After configuring, you'll have {total_after} provider(s).")
|
|
598
|
+
print("You need at least 2 providers for the 'consensus' tool.")
|
|
344
599
|
print("With only 1 provider, only basic tools will be available.")
|
|
345
600
|
proceed = questionary.confirm(
|
|
346
601
|
"Continue anyway?",
|
|
@@ -403,13 +658,14 @@ def setup_wizard():
|
|
|
403
658
|
style=custom_style,
|
|
404
659
|
).ask()
|
|
405
660
|
if show_manual:
|
|
406
|
-
print_manual_setup_instructions(selected)
|
|
661
|
+
print_manual_setup_instructions(selected, existing)
|
|
407
662
|
return
|
|
408
663
|
break
|
|
409
664
|
|
|
410
665
|
# Check final configuration
|
|
411
666
|
final_existing = check_existing_keys()
|
|
412
|
-
|
|
667
|
+
final_configured = [p for p, has_key in final_existing.items() if has_key]
|
|
668
|
+
final_count = len(final_configured)
|
|
413
669
|
|
|
414
670
|
print()
|
|
415
671
|
print("=" * 60)
|
|
@@ -417,36 +673,46 @@ def setup_wizard():
|
|
|
417
673
|
print("=" * 60)
|
|
418
674
|
print()
|
|
419
675
|
|
|
420
|
-
if not
|
|
676
|
+
if not final_configured:
|
|
421
677
|
print("No API keys were configured.")
|
|
422
678
|
print("Run 'second-opinion-mcp setup' to try again.")
|
|
423
679
|
return
|
|
424
680
|
|
|
425
|
-
print(f"Configured providers: {', '.join(
|
|
681
|
+
print(f"Configured providers: {', '.join(final_configured)} ({final_count} total)")
|
|
426
682
|
|
|
427
|
-
if
|
|
683
|
+
if final_count < 2:
|
|
428
684
|
print()
|
|
429
685
|
print("WARNING: Only 1 provider configured. The 'consensus' tool requires 2+.")
|
|
430
686
|
print("Run 'second-opinion-mcp setup' to add more providers.")
|
|
687
|
+
print()
|
|
688
|
+
return
|
|
431
689
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
default=False,
|
|
690
|
+
# Ask which client to show instructions for
|
|
691
|
+
client_choice = questionary.select(
|
|
692
|
+
"Which client will you use?",
|
|
693
|
+
choices=[
|
|
694
|
+
questionary.Choice("Claude Code CLI (Recommended)", value="claude-code"),
|
|
695
|
+
questionary.Choice("Claude Desktop App", value="claude-desktop"),
|
|
696
|
+
questionary.Choice("OpenClaw / Other MCP Client", value="other"),
|
|
697
|
+
questionary.Choice("Show all client instructions", value="all"),
|
|
698
|
+
],
|
|
442
699
|
style=custom_style,
|
|
443
700
|
).ask()
|
|
444
701
|
|
|
445
|
-
if
|
|
702
|
+
if client_choice == "all":
|
|
703
|
+
print_client_instructions(final_configured)
|
|
704
|
+
elif client_choice == "claude-code":
|
|
705
|
+
print()
|
|
706
|
+
print("Register with Claude Code CLI:")
|
|
707
|
+
print()
|
|
708
|
+
print(f" {get_registration_command(final_configured)}")
|
|
709
|
+
print()
|
|
710
|
+
print("Then restart Claude Code.")
|
|
711
|
+
elif client_choice == "claude-desktop":
|
|
446
712
|
print()
|
|
447
713
|
print("Add this to your claude_desktop_config.json:")
|
|
448
714
|
print()
|
|
449
|
-
print(get_desktop_config(
|
|
715
|
+
print(get_desktop_config(final_configured))
|
|
450
716
|
print()
|
|
451
717
|
if platform.system() == "Windows":
|
|
452
718
|
print("Location: %APPDATA%\\Claude\\claude_desktop_config.json")
|
|
@@ -454,6 +720,17 @@ def setup_wizard():
|
|
|
454
720
|
print("Location: ~/Library/Application Support/Claude/claude_desktop_config.json")
|
|
455
721
|
else:
|
|
456
722
|
print("Location: ~/.config/Claude/claude_desktop_config.json")
|
|
723
|
+
print()
|
|
724
|
+
print("Then restart Claude Desktop.")
|
|
725
|
+
else:
|
|
726
|
+
providers_env = ",".join(final_configured)
|
|
727
|
+
print()
|
|
728
|
+
print("For OpenClaw or other MCP clients:")
|
|
729
|
+
print()
|
|
730
|
+
print(" Server command: second-opinion-mcp")
|
|
731
|
+
print(f" Environment: SECOND_OPINION_PROVIDERS={providers_env}")
|
|
732
|
+
print()
|
|
733
|
+
print(" Add the server in your client's MCP settings with these values.")
|
|
457
734
|
|
|
458
735
|
print()
|
|
459
736
|
print("Test the server with:")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: second-opinion-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Multi-model AI analysis for Claude - get second opinions from DeepSeek, Kimi, and OpenRouter
|
|
5
5
|
Project-URL: Homepage, https://github.com/MarvinFS/second-opinion-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/MarvinFS/second-opinion-mcp
|
|
@@ -229,23 +229,26 @@ To update keys:
|
|
|
229
229
|
second-opinion-mcp setup
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
-
Or manually set each provider's key:
|
|
232
|
+
Or manually set each provider's key using the pipx environment Python:
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
|
+
# Use the pipx venv Python (required for pipx installations)
|
|
236
|
+
SOPYTHON=~/.local/share/pipx/venvs/second-opinion-mcp/bin/python3
|
|
237
|
+
|
|
235
238
|
# DeepSeek (key starts with sk-)
|
|
236
|
-
|
|
239
|
+
$SOPYTHON -c "import keyring; keyring.set_password('second-opinion', 'deepseek', 'sk-YOUR_DEEPSEEK_KEY')"
|
|
237
240
|
|
|
238
241
|
# Moonshot/Kimi
|
|
239
|
-
|
|
242
|
+
$SOPYTHON -c "import keyring; keyring.set_password('second-opinion', 'moonshot', 'YOUR_MOONSHOT_KEY')"
|
|
240
243
|
|
|
241
244
|
# OpenRouter (key starts with sk-or-)
|
|
242
|
-
|
|
245
|
+
$SOPYTHON -c "import keyring; keyring.set_password('second-opinion', 'openrouter', 'sk-or-YOUR_OPENROUTER_KEY')"
|
|
243
246
|
```
|
|
244
247
|
|
|
245
248
|
Verify keys are configured:
|
|
246
249
|
|
|
247
250
|
```bash
|
|
248
|
-
|
|
251
|
+
$SOPYTHON -c "import keyring; print('deepseek:', bool(keyring.get_password('second-opinion', 'deepseek'))); print('moonshot:', bool(keyring.get_password('second-opinion', 'moonshot'))); print('openrouter:', bool(keyring.get_password('second-opinion', 'openrouter')))"
|
|
249
252
|
```
|
|
250
253
|
|
|
251
254
|
### Claude Desktop Configuration
|
|
@@ -268,6 +271,15 @@ For Claude Desktop (the standalone app), add to `claude_desktop_config.json`:
|
|
|
268
271
|
}
|
|
269
272
|
```
|
|
270
273
|
|
|
274
|
+
### OpenClaw / Other MCP Clients
|
|
275
|
+
|
|
276
|
+
For OpenClaw or other MCP-compatible clients, add the server with these settings:
|
|
277
|
+
|
|
278
|
+
- Server command: `second-opinion-mcp`
|
|
279
|
+
- Environment variable: `SECOND_OPINION_PROVIDERS=deepseek,moonshot`
|
|
280
|
+
|
|
281
|
+
The setup wizard (`second-opinion-mcp setup`) will show specific instructions for your chosen client.
|
|
282
|
+
|
|
271
283
|
## Troubleshooting
|
|
272
284
|
|
|
273
285
|
### "No API key configured" Error
|
|
@@ -283,11 +295,19 @@ second-opinion-mcp setup
|
|
|
283
295
|
Linux without a desktop environment needs a keyring backend configured:
|
|
284
296
|
|
|
285
297
|
```bash
|
|
286
|
-
|
|
298
|
+
# Install keyrings.alt into the pipx environment
|
|
299
|
+
pipx inject second-opinion-mcp keyrings.alt
|
|
300
|
+
|
|
301
|
+
# Create the keyring config
|
|
287
302
|
mkdir -p ~/.local/share/python_keyring
|
|
288
|
-
|
|
303
|
+
cat > ~/.local/share/python_keyring/keyringrc.cfg << 'EOF'
|
|
304
|
+
[backend]
|
|
305
|
+
default-keyring=keyrings.alt.file.EncryptedKeyring
|
|
306
|
+
EOF
|
|
289
307
|
```
|
|
290
308
|
|
|
309
|
+
Then run `second-opinion-mcp setup` again.
|
|
310
|
+
|
|
291
311
|
### Connection Timeout
|
|
292
312
|
|
|
293
313
|
Check firewall, VPN, or proxy settings. Both DeepSeek (api.deepseek.com) and Moonshot (api.moonshot.ai) must be accessible.
|
|
@@ -367,13 +387,14 @@ Your API keys are preserved in the system keyring across updates.
|
|
|
367
387
|
# Remove from Claude
|
|
368
388
|
claude mcp remove second-opinion
|
|
369
389
|
|
|
390
|
+
# Optional: remove stored API keys BEFORE uninstalling (requires pipx venv Python)
|
|
391
|
+
SOPYTHON=~/.local/share/pipx/venvs/second-opinion-mcp/bin/python3
|
|
392
|
+
$SOPYTHON -c "import keyring; keyring.delete_password('second-opinion', 'deepseek')"
|
|
393
|
+
$SOPYTHON -c "import keyring; keyring.delete_password('second-opinion', 'moonshot')"
|
|
394
|
+
$SOPYTHON -c "import keyring; keyring.delete_password('second-opinion', 'openrouter')"
|
|
395
|
+
|
|
370
396
|
# Uninstall package
|
|
371
397
|
pipx uninstall second-opinion-mcp
|
|
372
|
-
|
|
373
|
-
# Optional: remove stored API keys
|
|
374
|
-
python3 -c "import keyring; keyring.delete_password('second-opinion', 'deepseek')"
|
|
375
|
-
python3 -c "import keyring; keyring.delete_password('second-opinion', 'moonshot')"
|
|
376
|
-
python3 -c "import keyring; keyring.delete_password('second-opinion', 'openrouter')"
|
|
377
398
|
```
|
|
378
399
|
|
|
379
400
|
## Security
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
second_opinion_mcp/__init__.py,sha256=BCfMuuqzzjjab7Q1s5H28CjwdqbWTtif-93rtyJj8eA,373
|
|
2
|
+
second_opinion_mcp/__main__.py,sha256=WDv19O6nvIH69GR_DUfXqDt-wm8bgsHNqvPm1LEIKzs,509
|
|
3
|
+
second_opinion_mcp/cli.py,sha256=fN6tdwhkkclE2r_gQyER7T7biXeaJyhTQ6QISp8sO8g,26775
|
|
4
|
+
second_opinion_mcp/server.py,sha256=46cHbK0GgL0VBCGI3gmTA1-vQ1jRDdQ6MsCCqWw3LVk,62237
|
|
5
|
+
second_opinion_mcp-0.3.5.dist-info/METADATA,sha256=qpSCCtu_UNZqPQkYM8HUL2lNkVXGgb70i_cQhMW-QeA,11588
|
|
6
|
+
second_opinion_mcp-0.3.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
second_opinion_mcp-0.3.5.dist-info/entry_points.txt,sha256=-E8BA2gFyU4qW-kJL8SV9Pg1Cc7glOCmraJoZH0PZP8,72
|
|
8
|
+
second_opinion_mcp-0.3.5.dist-info/licenses/LICENSE,sha256=dPx2Jy-Ejearvfh6IlF2PN4Srt-nZW8M4bW5EW7RPAg,1065
|
|
9
|
+
second_opinion_mcp-0.3.5.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
second_opinion_mcp/__init__.py,sha256=25qP5ShltFWoa09KfMQFCSGb6nOssroCDdNp8fEXQ6g,373
|
|
2
|
-
second_opinion_mcp/__main__.py,sha256=WDv19O6nvIH69GR_DUfXqDt-wm8bgsHNqvPm1LEIKzs,509
|
|
3
|
-
second_opinion_mcp/cli.py,sha256=KCT8-wEqMWzrjiLAYgwEdFUqWnkDDYgFm-eXCze4jDE,15281
|
|
4
|
-
second_opinion_mcp/server.py,sha256=46cHbK0GgL0VBCGI3gmTA1-vQ1jRDdQ6MsCCqWw3LVk,62237
|
|
5
|
-
second_opinion_mcp-0.3.4.dist-info/METADATA,sha256=IjrDmJyD6Z6WwHAxACRHQTIu6p7xBD3rbwD4rW4YAKI,10822
|
|
6
|
-
second_opinion_mcp-0.3.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
-
second_opinion_mcp-0.3.4.dist-info/entry_points.txt,sha256=-E8BA2gFyU4qW-kJL8SV9Pg1Cc7glOCmraJoZH0PZP8,72
|
|
8
|
-
second_opinion_mcp-0.3.4.dist-info/licenses/LICENSE,sha256=dPx2Jy-Ejearvfh6IlF2PN4Srt-nZW8M4bW5EW7RPAg,1065
|
|
9
|
-
second_opinion_mcp-0.3.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|