second-opinion-mcp 0.3.4__py3-none-any.whl → 0.3.6__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.
@@ -1,6 +1,6 @@
1
1
  """Second Opinion MCP Server - Multi-model AI analysis for Claude."""
2
2
 
3
- __version__ = "0.3.4"
3
+ __version__ = "0.3.6"
4
4
  __author__ = "MarvinFS"
5
5
 
6
6
  from second_opinion_mcp.server import mcp, second_opinion, challenge, code_review, consensus, review_synthesis
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
- print("For Linux, install one of these backends:")
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(" Option 1 - Secret Service (GNOME/KDE desktop):")
102
- print(" sudo apt install gnome-keyring # or kde-wallet")
103
- print(" pip install secretstorage")
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(" Option 2 - Encrypted file (headless/server):")
106
- print(" pip install keyrings.alt")
107
- print(" mkdir -p ~/.local/share/python_keyring")
108
- print(" cat > ~/.local/share/python_keyring/keyringrc.cfg << 'EOF'")
109
- print(" [backend]")
110
- print(" default-keyring=keyrings.alt.file.EncryptedKeyring")
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(" Then run 'second-opinion-mcp setup' again.")
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
- print(" pip install keyring --upgrade")
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,94 @@ 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
- print(" pip install keyring --upgrade")
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
- print("IMPORTANT: You must configure at least 2 providers for the")
142
- print("'consensus' tool to work. Single-provider mode is limited.")
143
- print()
144
- print("Run these commands to store your API keys:")
145
- print()
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
- print(f" # {config['name']} - Get key at {config['key_hint']}")
152
- print(f" python3 -c \"import keyring; keyring.set_password('{config['keyring_service']}', '{config['keyring_name']}', 'YOUR_API_KEY')\"")
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
- print(" python3 -c \"import keyring; print('deepseek:', bool(keyring.get_password('second-opinion', 'deepseek'))); print('moonshot:', bool(keyring.get_password('second-opinion', 'moonshot')))\"")
271
+ 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
272
  print()
159
- print("After configuring keys, register with Claude Code:")
273
+ print("After configuring at least 2 keys, register with Claude Code:")
160
274
  print()
161
275
 
162
276
  if providers and len(providers) >= 1:
@@ -234,6 +348,47 @@ def get_desktop_config(selected_providers: list[str]) -> str:
234
348
  }}'''
235
349
 
236
350
 
351
+ def print_client_instructions(configured_providers: list[str]):
352
+ """Print setup instructions for all supported MCP clients."""
353
+ providers_env = ",".join(configured_providers)
354
+
355
+ print()
356
+ print("=" * 60)
357
+ print(" Client Setup Instructions")
358
+ print("=" * 60)
359
+
360
+ # Claude Code CLI
361
+ print()
362
+ print("CLAUDE CODE CLI:")
363
+ print(f" claude mcp add -s user second-opinion -e SECOND_OPINION_PROVIDERS=\"{providers_env}\" -- second-opinion-mcp")
364
+
365
+ # Claude Desktop
366
+ print()
367
+ print("CLAUDE DESKTOP APP:")
368
+ print(" Add to claude_desktop_config.json:")
369
+ if platform.system() == "Windows":
370
+ print(" Location: %APPDATA%\\Claude\\claude_desktop_config.json")
371
+ elif platform.system() == "Darwin":
372
+ print(" Location: ~/Library/Application Support/Claude/claude_desktop_config.json")
373
+ else:
374
+ print(" Location: ~/.config/Claude/claude_desktop_config.json")
375
+ print()
376
+ print(get_desktop_config(configured_providers))
377
+
378
+ # OpenClaw / Other MCP clients
379
+ print()
380
+ print("OPENCLAW / OTHER MCP CLIENTS:")
381
+ print(" Server command: second-opinion-mcp")
382
+ print(f" Environment: SECOND_OPINION_PROVIDERS={providers_env}")
383
+ print()
384
+ print(" For OpenClaw, add in settings with:")
385
+ print(" - Name: second-opinion")
386
+ print(" - Command: second-opinion-mcp")
387
+ print(f" - Environment variables: SECOND_OPINION_PROVIDERS={providers_env}")
388
+
389
+ print()
390
+
391
+
237
392
  def setup_wizard():
238
393
  """Interactive setup wizard for second-opinion-mcp."""
239
394
  print()
@@ -272,12 +427,89 @@ def setup_wizard():
272
427
  # Check existing configuration
273
428
  existing = check_existing_keys()
274
429
  configured_count = sum(1 for v in existing.values() if v)
430
+ configured = [p for p, has_key in existing.items() if has_key]
431
+ not_configured = [p for p, has_key in existing.items() if not has_key]
275
432
 
276
- if configured_count > 0:
277
- configured = [p for p, has_key in existing.items() if has_key]
278
- print(f"Already configured: {', '.join(configured)}")
279
- if configured_count < 2:
280
- print("Note: At least 2 providers required for 'consensus' tool.")
433
+ # Show current status
434
+ print("Current status:")
435
+ for provider, config in PROVIDERS.items():
436
+ status = "configured" if existing.get(provider) else "not configured"
437
+ print(f" {config['name']}: {status}")
438
+ print()
439
+
440
+ # If 2+ providers already configured, offer quick path
441
+ if configured_count >= 2:
442
+ print(f"You have {configured_count} providers configured (minimum 2 required).")
443
+ print()
444
+
445
+ action = questionary.select(
446
+ "What would you like to do?",
447
+ choices=[
448
+ questionary.Choice("Show registration command (ready to use)", value="register"),
449
+ questionary.Choice("Add or update API keys", value="configure"),
450
+ questionary.Choice("Manual setup (show CLI commands)", value="manual"),
451
+ ],
452
+ style=custom_style,
453
+ ).ask()
454
+
455
+ if action == "register":
456
+ client_choice = questionary.select(
457
+ "Which client will you use?",
458
+ choices=[
459
+ questionary.Choice("Claude Code CLI (Recommended)", value="claude-code"),
460
+ questionary.Choice("Claude Desktop App", value="claude-desktop"),
461
+ questionary.Choice("OpenClaw / Other MCP Client", value="other"),
462
+ questionary.Choice("Show all client instructions", value="all"),
463
+ ],
464
+ style=custom_style,
465
+ ).ask()
466
+
467
+ if client_choice == "all":
468
+ print_client_instructions(configured)
469
+ elif client_choice == "claude-code":
470
+ print()
471
+ print("Register with Claude Code CLI:")
472
+ print()
473
+ print(f" {get_registration_command(configured)}")
474
+ print()
475
+ print("Then restart Claude Code.")
476
+ elif client_choice == "claude-desktop":
477
+ print()
478
+ print("Add this to your claude_desktop_config.json:")
479
+ print()
480
+ print(get_desktop_config(configured))
481
+ print()
482
+ if platform.system() == "Windows":
483
+ print("Location: %APPDATA%\\Claude\\claude_desktop_config.json")
484
+ elif platform.system() == "Darwin":
485
+ print("Location: ~/Library/Application Support/Claude/claude_desktop_config.json")
486
+ else:
487
+ print("Location: ~/.config/Claude/claude_desktop_config.json")
488
+ print()
489
+ print("Then restart Claude Desktop.")
490
+ else:
491
+ providers_env = ",".join(configured)
492
+ print()
493
+ print("For OpenClaw or other MCP clients:")
494
+ print()
495
+ print(" Server command: second-opinion-mcp")
496
+ print(f" Environment: SECOND_OPINION_PROVIDERS={providers_env}")
497
+ print()
498
+ print(" Add the server in your client's MCP settings with these values.")
499
+ return
500
+
501
+ elif action == "manual":
502
+ print_manual_setup_instructions(list(PROVIDERS.keys()), existing)
503
+ return
504
+
505
+ # Fall through to configure flow
506
+
507
+ elif configured_count == 1:
508
+ print(f"Only {configured_count} provider configured. You need at least 2.")
509
+ print("The 'consensus' tool requires multiple models to debate.")
510
+ print()
511
+ else:
512
+ print("No providers configured yet. You need at least 2.")
281
513
  print()
282
514
 
283
515
  # Setup method selection
@@ -314,33 +546,55 @@ def setup_wizard():
314
546
  if not selected:
315
547
  selected = list(PROVIDERS.keys())
316
548
 
317
- print_manual_setup_instructions(selected)
549
+ print_manual_setup_instructions(selected, existing)
318
550
  return
319
551
 
320
- # Interactive setup
321
- choices = []
322
- for provider, config in PROVIDERS.items():
323
- label = config["name"]
324
- if existing.get(provider):
325
- label += " (configured)"
326
- choices.append(questionary.Choice(label, value=provider, checked=not existing.get(provider)))
552
+ # Interactive setup - determine which providers to configure
553
+ if not_configured:
554
+ # Default: configure unconfigured providers
555
+ choices = []
556
+ for provider, config in PROVIDERS.items():
557
+ label = config["name"]
558
+ if existing.get(provider):
559
+ label += " (configured)"
560
+ # Pre-check unconfigured ones, or all if less than 2 configured
561
+ should_check = not existing.get(provider)
562
+ choices.append(questionary.Choice(label, value=provider, checked=should_check))
563
+
564
+ # Calculate how many more we need
565
+ needed = max(0, 2 - configured_count)
327
566
 
328
- selected = questionary.checkbox(
329
- "Select providers to configure (at least 2 required for consensus):",
330
- choices=choices,
331
- style=custom_style,
332
- validate=lambda x: len(x) >= 1 or "Select at least one provider",
333
- ).ask()
567
+ selected = questionary.checkbox(
568
+ f"Select providers to configure (need {needed} more for minimum 2):",
569
+ choices=choices,
570
+ style=custom_style,
571
+ validate=lambda x: len(x) >= 1 or "Select at least one provider",
572
+ ).ask()
573
+ else:
574
+ # All configured, but user wants to update
575
+ choices = [
576
+ questionary.Choice(f"{config['name']} (configured)", value=provider)
577
+ for provider, config in PROVIDERS.items()
578
+ ]
579
+
580
+ selected = questionary.checkbox(
581
+ "Select providers to update:",
582
+ choices=choices,
583
+ style=custom_style,
584
+ ).ask()
334
585
 
335
586
  if not selected:
336
587
  print("Setup cancelled.")
337
588
  return
338
589
 
339
- # Warn about single provider
340
- total_will_have = len([p for p in selected if not existing.get(p)]) + configured_count
341
- if total_will_have < 2 and len(selected) < 2:
590
+ # Check if this will meet minimum requirement
591
+ will_configure_new = len([p for p in selected if not existing.get(p)])
592
+ total_after = configured_count + will_configure_new
593
+
594
+ if total_after < 2 and will_configure_new > 0:
342
595
  print()
343
- print("WARNING: You need at least 2 providers for the 'consensus' tool.")
596
+ print(f"WARNING: After configuring, you'll have {total_after} provider(s).")
597
+ print("You need at least 2 providers for the 'consensus' tool.")
344
598
  print("With only 1 provider, only basic tools will be available.")
345
599
  proceed = questionary.confirm(
346
600
  "Continue anyway?",
@@ -403,13 +657,14 @@ def setup_wizard():
403
657
  style=custom_style,
404
658
  ).ask()
405
659
  if show_manual:
406
- print_manual_setup_instructions(selected)
660
+ print_manual_setup_instructions(selected, existing)
407
661
  return
408
662
  break
409
663
 
410
664
  # Check final configuration
411
665
  final_existing = check_existing_keys()
412
- configured = [p for p, has_key in final_existing.items() if has_key]
666
+ final_configured = [p for p, has_key in final_existing.items() if has_key]
667
+ final_count = len(final_configured)
413
668
 
414
669
  print()
415
670
  print("=" * 60)
@@ -417,36 +672,46 @@ def setup_wizard():
417
672
  print("=" * 60)
418
673
  print()
419
674
 
420
- if not configured:
675
+ if not final_configured:
421
676
  print("No API keys were configured.")
422
677
  print("Run 'second-opinion-mcp setup' to try again.")
423
678
  return
424
679
 
425
- print(f"Configured providers: {', '.join(configured)}")
680
+ print(f"Configured providers: {', '.join(final_configured)} ({final_count} total)")
426
681
 
427
- if len(configured) < 2:
682
+ if final_count < 2:
428
683
  print()
429
684
  print("WARNING: Only 1 provider configured. The 'consensus' tool requires 2+.")
430
685
  print("Run 'second-opinion-mcp setup' to add more providers.")
686
+ print()
687
+ return
431
688
 
432
- print()
433
- print("Register with Claude Code CLI:")
434
- print()
435
- print(f" {get_registration_command(configured)}")
436
- print()
437
-
438
- # Show desktop config option
439
- show_desktop = questionary.confirm(
440
- "Show Claude Desktop configuration?",
441
- default=False,
689
+ # Ask which client to show instructions for
690
+ client_choice = questionary.select(
691
+ "Which client will you use?",
692
+ choices=[
693
+ questionary.Choice("Claude Code CLI (Recommended)", value="claude-code"),
694
+ questionary.Choice("Claude Desktop App", value="claude-desktop"),
695
+ questionary.Choice("OpenClaw / Other MCP Client", value="other"),
696
+ questionary.Choice("Show all client instructions", value="all"),
697
+ ],
442
698
  style=custom_style,
443
699
  ).ask()
444
700
 
445
- if show_desktop:
701
+ if client_choice == "all":
702
+ print_client_instructions(final_configured)
703
+ elif client_choice == "claude-code":
704
+ print()
705
+ print("Register with Claude Code CLI:")
706
+ print()
707
+ print(f" {get_registration_command(final_configured)}")
708
+ print()
709
+ print("Then restart Claude Code.")
710
+ elif client_choice == "claude-desktop":
446
711
  print()
447
712
  print("Add this to your claude_desktop_config.json:")
448
713
  print()
449
- print(get_desktop_config(configured))
714
+ print(get_desktop_config(final_configured))
450
715
  print()
451
716
  if platform.system() == "Windows":
452
717
  print("Location: %APPDATA%\\Claude\\claude_desktop_config.json")
@@ -454,6 +719,17 @@ def setup_wizard():
454
719
  print("Location: ~/Library/Application Support/Claude/claude_desktop_config.json")
455
720
  else:
456
721
  print("Location: ~/.config/Claude/claude_desktop_config.json")
722
+ print()
723
+ print("Then restart Claude Desktop.")
724
+ else:
725
+ providers_env = ",".join(final_configured)
726
+ print()
727
+ print("For OpenClaw or other MCP clients:")
728
+ print()
729
+ print(" Server command: second-opinion-mcp")
730
+ print(f" Environment: SECOND_OPINION_PROVIDERS={providers_env}")
731
+ print()
732
+ print(" Add the server in your client's MCP settings with these values.")
457
733
 
458
734
  print()
459
735
  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.4
3
+ Version: 0.3.6
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
- python3 -c "import keyring; keyring.set_password('second-opinion', 'deepseek', 'sk-YOUR_DEEPSEEK_KEY')"
239
+ $SOPYTHON -c "import keyring; keyring.set_password('second-opinion', 'deepseek', 'sk-YOUR_DEEPSEEK_KEY')"
237
240
 
238
241
  # Moonshot/Kimi
239
- python3 -c "import keyring; keyring.set_password('second-opinion', 'moonshot', 'YOUR_MOONSHOT_KEY')"
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
- python3 -c "import keyring; keyring.set_password('second-opinion', 'openrouter', 'sk-or-YOUR_OPENROUTER_KEY')"
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
- python3 -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')))"
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
- pip install keyrings.alt
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
- echo -e "[backend]\ndefault-keyring=keyrings.alt.file.EncryptedKeyring" > ~/.local/share/python_keyring/keyringrc.cfg
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=BJBLikFUnc016tLagTu4T11GSPMyBTqWG7uRXkbu8XU,373
2
+ second_opinion_mcp/__main__.py,sha256=WDv19O6nvIH69GR_DUfXqDt-wm8bgsHNqvPm1LEIKzs,509
3
+ second_opinion_mcp/cli.py,sha256=CzUKTAhKOUcEYoECaybtsQoz8crzZRBRMjSVZNMGNQo,26608
4
+ second_opinion_mcp/server.py,sha256=46cHbK0GgL0VBCGI3gmTA1-vQ1jRDdQ6MsCCqWw3LVk,62237
5
+ second_opinion_mcp-0.3.6.dist-info/METADATA,sha256=aT0t3kPUBCFj0dUJkvnOU83_thavyggHfDr-1KA6-P8,11588
6
+ second_opinion_mcp-0.3.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
7
+ second_opinion_mcp-0.3.6.dist-info/entry_points.txt,sha256=-E8BA2gFyU4qW-kJL8SV9Pg1Cc7glOCmraJoZH0PZP8,72
8
+ second_opinion_mcp-0.3.6.dist-info/licenses/LICENSE,sha256=dPx2Jy-Ejearvfh6IlF2PN4Srt-nZW8M4bW5EW7RPAg,1065
9
+ second_opinion_mcp-0.3.6.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,,