second-opinion-mcp 0.3.0__py3-none-any.whl → 0.3.1__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 +242 -49
- second_opinion_mcp/server.py +65 -0
- {second_opinion_mcp-0.3.0.dist-info → second_opinion_mcp-0.3.1.dist-info}/METADATA +1 -1
- second_opinion_mcp-0.3.1.dist-info/RECORD +9 -0
- second_opinion_mcp-0.3.0.dist-info/RECORD +0 -9
- {second_opinion_mcp-0.3.0.dist-info → second_opinion_mcp-0.3.1.dist-info}/WHEEL +0 -0
- {second_opinion_mcp-0.3.0.dist-info → second_opinion_mcp-0.3.1.dist-info}/entry_points.txt +0 -0
- {second_opinion_mcp-0.3.0.dist-info → second_opinion_mcp-0.3.1.dist-info}/licenses/LICENSE +0 -0
second_opinion_mcp/__init__.py
CHANGED
second_opinion_mcp/cli.py
CHANGED
|
@@ -11,6 +11,7 @@ except ImportError:
|
|
|
11
11
|
sys.exit(1)
|
|
12
12
|
|
|
13
13
|
import keyring
|
|
14
|
+
from keyring.errors import NoKeyringError
|
|
14
15
|
|
|
15
16
|
# Provider configurations
|
|
16
17
|
PROVIDERS = {
|
|
@@ -19,7 +20,7 @@ PROVIDERS = {
|
|
|
19
20
|
"description": "DeepSeek Reasoner with chain-of-thought reasoning",
|
|
20
21
|
"keyring_service": "second-opinion",
|
|
21
22
|
"keyring_name": "deepseek",
|
|
22
|
-
"key_hint": "
|
|
23
|
+
"key_hint": "https://platform.deepseek.com",
|
|
23
24
|
"key_prefix": "sk-",
|
|
24
25
|
},
|
|
25
26
|
"moonshot": {
|
|
@@ -27,7 +28,7 @@ PROVIDERS = {
|
|
|
27
28
|
"description": "Moonshot AI's Kimi with thinking mode",
|
|
28
29
|
"keyring_service": "second-opinion",
|
|
29
30
|
"keyring_name": "moonshot",
|
|
30
|
-
"key_hint": "
|
|
31
|
+
"key_hint": "https://platform.moonshot.cn",
|
|
31
32
|
"key_prefix": None,
|
|
32
33
|
},
|
|
33
34
|
"openrouter": {
|
|
@@ -35,7 +36,7 @@ PROVIDERS = {
|
|
|
35
36
|
"description": "Access 300+ models via unified API",
|
|
36
37
|
"keyring_service": "second-opinion",
|
|
37
38
|
"keyring_name": "openrouter",
|
|
38
|
-
"key_hint": "
|
|
39
|
+
"key_hint": "https://openrouter.ai/keys",
|
|
39
40
|
"key_prefix": "sk-or-",
|
|
40
41
|
},
|
|
41
42
|
}
|
|
@@ -51,6 +52,122 @@ custom_style = Style([
|
|
|
51
52
|
])
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
def check_keyring_available() -> tuple[bool, str]:
|
|
56
|
+
"""Check if keyring backend is available and working."""
|
|
57
|
+
try:
|
|
58
|
+
# Try to access keyring - this will fail if no backend
|
|
59
|
+
backend = keyring.get_keyring()
|
|
60
|
+
backend_name = str(type(backend).__name__)
|
|
61
|
+
|
|
62
|
+
# Check for null/fail backends
|
|
63
|
+
if "Null" in backend_name or "Fail" in backend_name:
|
|
64
|
+
return False, backend_name
|
|
65
|
+
|
|
66
|
+
# Try a test write/read/delete to verify it works
|
|
67
|
+
test_service = "second-opinion-test"
|
|
68
|
+
test_key = "connectivity-check"
|
|
69
|
+
try:
|
|
70
|
+
keyring.set_password(test_service, test_key, "test")
|
|
71
|
+
result = keyring.get_password(test_service, test_key)
|
|
72
|
+
keyring.delete_password(test_service, test_key)
|
|
73
|
+
if result != "test":
|
|
74
|
+
return False, backend_name
|
|
75
|
+
except Exception:
|
|
76
|
+
return False, backend_name
|
|
77
|
+
|
|
78
|
+
return True, backend_name
|
|
79
|
+
except NoKeyringError:
|
|
80
|
+
return False, "NoKeyringError"
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return False, str(e)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def print_keyring_setup_instructions():
|
|
86
|
+
"""Print instructions for setting up keyring on different platforms."""
|
|
87
|
+
print()
|
|
88
|
+
print("=" * 60)
|
|
89
|
+
print(" Keyring Backend Required")
|
|
90
|
+
print("=" * 60)
|
|
91
|
+
print()
|
|
92
|
+
print("second-opinion-mcp stores API keys securely in your system keyring.")
|
|
93
|
+
print("A keyring backend must be installed and configured first.")
|
|
94
|
+
print()
|
|
95
|
+
|
|
96
|
+
system = platform.system()
|
|
97
|
+
|
|
98
|
+
if system == "Linux":
|
|
99
|
+
print("For Linux, install one of these backends:")
|
|
100
|
+
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")
|
|
104
|
+
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")
|
|
112
|
+
print()
|
|
113
|
+
print(" Then run 'second-opinion-mcp setup' again.")
|
|
114
|
+
|
|
115
|
+
elif system == "Darwin":
|
|
116
|
+
print("For macOS, keyring should work automatically with Keychain.")
|
|
117
|
+
print("If you see this error, try:")
|
|
118
|
+
print()
|
|
119
|
+
print(" pip install keyring --upgrade")
|
|
120
|
+
print()
|
|
121
|
+
print("Then run 'second-opinion-mcp setup' again.")
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
print("For Windows, keyring should work automatically with Credential Manager.")
|
|
125
|
+
print("If you see this error, try:")
|
|
126
|
+
print()
|
|
127
|
+
print(" pip install keyring --upgrade")
|
|
128
|
+
print()
|
|
129
|
+
print("Then run 'second-opinion-mcp setup' again.")
|
|
130
|
+
|
|
131
|
+
print()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def print_manual_setup_instructions(providers: list[str] | None = None):
|
|
135
|
+
"""Print instructions for manual keyring setup."""
|
|
136
|
+
print()
|
|
137
|
+
print("=" * 60)
|
|
138
|
+
print(" Manual API Key Setup")
|
|
139
|
+
print("=" * 60)
|
|
140
|
+
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()
|
|
146
|
+
|
|
147
|
+
target_providers = providers if providers else list(PROVIDERS.keys())
|
|
148
|
+
|
|
149
|
+
for provider in target_providers:
|
|
150
|
+
config = PROVIDERS[provider]
|
|
151
|
+
print(f" # {config['name']} - Get key at {config['key_hint']}")
|
|
152
|
+
print(f" python -c \"import keyring; keyring.set_password('{config['keyring_service']}', '{config['keyring_name']}', 'YOUR_API_KEY')\"")
|
|
153
|
+
print()
|
|
154
|
+
|
|
155
|
+
print("To verify your keys are stored:")
|
|
156
|
+
print()
|
|
157
|
+
print(" python -c \"import keyring; print('deepseek:', bool(keyring.get_password('second-opinion', 'deepseek'))); print('moonshot:', bool(keyring.get_password('second-opinion', 'moonshot')))\"")
|
|
158
|
+
print()
|
|
159
|
+
print("After configuring keys, register with Claude Code:")
|
|
160
|
+
print()
|
|
161
|
+
|
|
162
|
+
if providers and len(providers) >= 1:
|
|
163
|
+
providers_str = ",".join(providers)
|
|
164
|
+
else:
|
|
165
|
+
providers_str = "deepseek,moonshot"
|
|
166
|
+
|
|
167
|
+
print(f" claude mcp add -s user second-opinion -e SECOND_OPINION_PROVIDERS=\"{providers_str}\" -- second-opinion-mcp")
|
|
168
|
+
print()
|
|
169
|
+
|
|
170
|
+
|
|
54
171
|
def check_existing_keys() -> dict[str, bool]:
|
|
55
172
|
"""Check which providers already have API keys configured."""
|
|
56
173
|
existing = {}
|
|
@@ -63,15 +180,19 @@ def check_existing_keys() -> dict[str, bool]:
|
|
|
63
180
|
return existing
|
|
64
181
|
|
|
65
182
|
|
|
66
|
-
def
|
|
67
|
-
"""
|
|
183
|
+
def count_configured_providers() -> int:
|
|
184
|
+
"""Count how many providers have API keys configured."""
|
|
185
|
+
return sum(1 for v in check_existing_keys().values() if v)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def store_api_key(provider: str, key: str) -> tuple[bool, str]:
|
|
189
|
+
"""Store API key in system keyring. Returns (success, error_message)."""
|
|
68
190
|
config = PROVIDERS[provider]
|
|
69
191
|
try:
|
|
70
192
|
keyring.set_password(config["keyring_service"], config["keyring_name"], key)
|
|
71
|
-
return True
|
|
193
|
+
return True, ""
|
|
72
194
|
except Exception as e:
|
|
73
|
-
|
|
74
|
-
return False
|
|
195
|
+
return False, str(e)
|
|
75
196
|
|
|
76
197
|
|
|
77
198
|
def validate_api_key(provider: str, key: str) -> tuple[bool, str]:
|
|
@@ -94,11 +215,7 @@ def validate_api_key(provider: str, key: str) -> tuple[bool, str]:
|
|
|
94
215
|
def get_registration_command(selected_providers: list[str]) -> str:
|
|
95
216
|
"""Generate the registration command for Claude Code CLI."""
|
|
96
217
|
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'''
|
|
218
|
+
return f'claude mcp add -s user second-opinion -e SECOND_OPINION_PROVIDERS="{providers_env}" -- second-opinion-mcp'
|
|
102
219
|
|
|
103
220
|
|
|
104
221
|
def get_desktop_config(selected_providers: list[str]) -> str:
|
|
@@ -124,29 +241,92 @@ def setup_wizard():
|
|
|
124
241
|
print(" Second Opinion MCP - Setup Wizard")
|
|
125
242
|
print("=" * 60)
|
|
126
243
|
print()
|
|
244
|
+
|
|
245
|
+
# Check keyring availability first
|
|
246
|
+
keyring_ok, backend_info = check_keyring_available()
|
|
247
|
+
|
|
248
|
+
if not keyring_ok:
|
|
249
|
+
print(f"Keyring backend not available: {backend_info}")
|
|
250
|
+
print_keyring_setup_instructions()
|
|
251
|
+
print()
|
|
252
|
+
print("Alternatively, choose 'Manual setup' to get CLI commands.")
|
|
253
|
+
print()
|
|
254
|
+
|
|
255
|
+
choice = questionary.select(
|
|
256
|
+
"How would you like to proceed?",
|
|
257
|
+
choices=[
|
|
258
|
+
questionary.Choice("Manual setup (show CLI commands)", value="manual"),
|
|
259
|
+
questionary.Choice("Exit and fix keyring first", value="exit"),
|
|
260
|
+
],
|
|
261
|
+
style=custom_style,
|
|
262
|
+
).ask()
|
|
263
|
+
|
|
264
|
+
if choice == "manual" or choice is None:
|
|
265
|
+
print_manual_setup_instructions()
|
|
266
|
+
return
|
|
267
|
+
|
|
127
268
|
print("This wizard will help you configure API keys for AI providers.")
|
|
128
269
|
print("Keys are stored securely in your system keyring.")
|
|
129
270
|
print()
|
|
130
271
|
|
|
131
272
|
# Check existing configuration
|
|
132
273
|
existing = check_existing_keys()
|
|
133
|
-
|
|
274
|
+
configured_count = sum(1 for v in existing.values() if v)
|
|
134
275
|
|
|
135
|
-
if
|
|
276
|
+
if configured_count > 0:
|
|
136
277
|
configured = [p for p, has_key in existing.items() if has_key]
|
|
137
278
|
print(f"Already configured: {', '.join(configured)}")
|
|
279
|
+
if configured_count < 2:
|
|
280
|
+
print("Note: At least 2 providers required for 'consensus' tool.")
|
|
138
281
|
print()
|
|
139
282
|
|
|
140
|
-
#
|
|
283
|
+
# Setup method selection
|
|
284
|
+
setup_method = questionary.select(
|
|
285
|
+
"Setup method:",
|
|
286
|
+
choices=[
|
|
287
|
+
questionary.Choice("Interactive (enter keys now)", value="interactive"),
|
|
288
|
+
questionary.Choice("Manual (show CLI commands)", value="manual"),
|
|
289
|
+
],
|
|
290
|
+
style=custom_style,
|
|
291
|
+
).ask()
|
|
292
|
+
|
|
293
|
+
if setup_method is None:
|
|
294
|
+
print("Setup cancelled.")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
if setup_method == "manual":
|
|
298
|
+
# Ask which providers they want to configure
|
|
299
|
+
choices = [
|
|
300
|
+
questionary.Choice(
|
|
301
|
+
f"{config['name']}" + (" (configured)" if existing.get(provider) else ""),
|
|
302
|
+
value=provider,
|
|
303
|
+
checked=not existing.get(provider) # Pre-check unconfigured ones
|
|
304
|
+
)
|
|
305
|
+
for provider, config in PROVIDERS.items()
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
selected = questionary.checkbox(
|
|
309
|
+
"Select providers to configure:",
|
|
310
|
+
choices=choices,
|
|
311
|
+
style=custom_style,
|
|
312
|
+
).ask()
|
|
313
|
+
|
|
314
|
+
if not selected:
|
|
315
|
+
selected = list(PROVIDERS.keys())
|
|
316
|
+
|
|
317
|
+
print_manual_setup_instructions(selected)
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
# Interactive setup
|
|
141
321
|
choices = []
|
|
142
322
|
for provider, config in PROVIDERS.items():
|
|
143
323
|
label = config["name"]
|
|
144
324
|
if existing.get(provider):
|
|
145
325
|
label += " (configured)"
|
|
146
|
-
choices.append(questionary.Choice(label, value=provider, checked=existing.get(provider
|
|
326
|
+
choices.append(questionary.Choice(label, value=provider, checked=not existing.get(provider)))
|
|
147
327
|
|
|
148
328
|
selected = questionary.checkbox(
|
|
149
|
-
"Select providers to configure (at least 2
|
|
329
|
+
"Select providers to configure (at least 2 required for consensus):",
|
|
150
330
|
choices=choices,
|
|
151
331
|
style=custom_style,
|
|
152
332
|
validate=lambda x: len(x) >= 1 or "Select at least one provider",
|
|
@@ -156,20 +336,24 @@ def setup_wizard():
|
|
|
156
336
|
print("Setup cancelled.")
|
|
157
337
|
return
|
|
158
338
|
|
|
159
|
-
|
|
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:
|
|
160
342
|
print()
|
|
161
|
-
print("
|
|
343
|
+
print("WARNING: You need at least 2 providers for the 'consensus' tool.")
|
|
344
|
+
print("With only 1 provider, only basic tools will be available.")
|
|
162
345
|
proceed = questionary.confirm(
|
|
163
|
-
"Continue
|
|
164
|
-
default=
|
|
346
|
+
"Continue anyway?",
|
|
347
|
+
default=False,
|
|
165
348
|
style=custom_style,
|
|
166
349
|
).ask()
|
|
167
350
|
if not proceed:
|
|
168
|
-
return setup_wizard()
|
|
351
|
+
return setup_wizard()
|
|
169
352
|
|
|
170
353
|
print()
|
|
171
354
|
|
|
172
355
|
# Configure API keys
|
|
356
|
+
keyring_error_shown = False
|
|
173
357
|
for provider in selected:
|
|
174
358
|
config = PROVIDERS[provider]
|
|
175
359
|
|
|
@@ -183,15 +367,15 @@ def setup_wizard():
|
|
|
183
367
|
continue
|
|
184
368
|
|
|
185
369
|
print(f"\n{config['name']}")
|
|
186
|
-
print(f" {config['key_hint']}")
|
|
370
|
+
print(f" Get your key at: {config['key_hint']}")
|
|
187
371
|
|
|
188
372
|
while True:
|
|
189
373
|
key = questionary.password(
|
|
190
|
-
|
|
374
|
+
" Enter API key:",
|
|
191
375
|
style=custom_style,
|
|
192
376
|
).ask()
|
|
193
377
|
|
|
194
|
-
if key is None:
|
|
378
|
+
if key is None:
|
|
195
379
|
print(" Skipped.")
|
|
196
380
|
break
|
|
197
381
|
|
|
@@ -203,44 +387,55 @@ def setup_wizard():
|
|
|
203
387
|
break
|
|
204
388
|
continue
|
|
205
389
|
|
|
206
|
-
|
|
390
|
+
success, store_error = store_api_key(provider, key.strip())
|
|
391
|
+
if success:
|
|
207
392
|
print(" Key stored successfully.")
|
|
208
|
-
|
|
393
|
+
break
|
|
394
|
+
else:
|
|
395
|
+
print(f" Error storing key: {store_error}")
|
|
396
|
+
if not keyring_error_shown:
|
|
397
|
+
keyring_error_shown = True
|
|
398
|
+
print()
|
|
399
|
+
print(" Keyring storage failed. You may need to configure a backend.")
|
|
400
|
+
show_manual = questionary.confirm(
|
|
401
|
+
" Show manual setup instructions?",
|
|
402
|
+
default=True,
|
|
403
|
+
style=custom_style,
|
|
404
|
+
).ask()
|
|
405
|
+
if show_manual:
|
|
406
|
+
print_manual_setup_instructions(selected)
|
|
407
|
+
return
|
|
408
|
+
break
|
|
409
|
+
|
|
410
|
+
# Check final configuration
|
|
411
|
+
final_existing = check_existing_keys()
|
|
412
|
+
configured = [p for p, has_key in final_existing.items() if has_key]
|
|
209
413
|
|
|
210
|
-
# Generate registration commands
|
|
211
414
|
print()
|
|
212
415
|
print("=" * 60)
|
|
213
416
|
print(" Setup Complete!")
|
|
214
417
|
print("=" * 60)
|
|
215
418
|
print()
|
|
216
419
|
|
|
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
420
|
if not configured:
|
|
231
|
-
print("No API keys were configured.
|
|
421
|
+
print("No API keys were configured.")
|
|
422
|
+
print("Run 'second-opinion-mcp setup' to try again.")
|
|
232
423
|
return
|
|
233
424
|
|
|
234
425
|
print(f"Configured providers: {', '.join(configured)}")
|
|
235
|
-
print()
|
|
236
426
|
|
|
237
|
-
|
|
238
|
-
|
|
427
|
+
if len(configured) < 2:
|
|
428
|
+
print()
|
|
429
|
+
print("WARNING: Only 1 provider configured. The 'consensus' tool requires 2+.")
|
|
430
|
+
print("Run 'second-opinion-mcp setup' to add more providers.")
|
|
431
|
+
|
|
432
|
+
print()
|
|
433
|
+
print("Register with Claude Code CLI:")
|
|
239
434
|
print()
|
|
240
435
|
print(f" {get_registration_command(configured)}")
|
|
241
436
|
print()
|
|
242
437
|
|
|
243
|
-
# Show desktop config
|
|
438
|
+
# Show desktop config option
|
|
244
439
|
show_desktop = questionary.confirm(
|
|
245
440
|
"Show Claude Desktop configuration?",
|
|
246
441
|
default=False,
|
|
@@ -264,8 +459,6 @@ def setup_wizard():
|
|
|
264
459
|
print("Test the server with:")
|
|
265
460
|
print(" second-opinion-mcp")
|
|
266
461
|
print()
|
|
267
|
-
print("The server will start and wait for input. Press Ctrl+C to stop.")
|
|
268
|
-
print()
|
|
269
462
|
|
|
270
463
|
|
|
271
464
|
if __name__ == "__main__":
|
second_opinion_mcp/server.py
CHANGED
|
@@ -1523,13 +1523,78 @@ def _sync_cleanup():
|
|
|
1523
1523
|
atexit.register(_sync_cleanup)
|
|
1524
1524
|
|
|
1525
1525
|
|
|
1526
|
+
def _check_provider_keys() -> tuple[list[str], list[str]]:
|
|
1527
|
+
"""Check which enabled providers have API keys configured.
|
|
1528
|
+
|
|
1529
|
+
Returns:
|
|
1530
|
+
(configured_providers, missing_providers) tuple
|
|
1531
|
+
"""
|
|
1532
|
+
configured = []
|
|
1533
|
+
missing = []
|
|
1534
|
+
|
|
1535
|
+
for provider, config in MODELS.items():
|
|
1536
|
+
if not config.get("enabled"):
|
|
1537
|
+
continue
|
|
1538
|
+
|
|
1539
|
+
try:
|
|
1540
|
+
key = keyring.get_password(config["keyring_service"], config["keyring_name"])
|
|
1541
|
+
if key:
|
|
1542
|
+
configured.append(provider)
|
|
1543
|
+
else:
|
|
1544
|
+
missing.append(provider)
|
|
1545
|
+
except Exception:
|
|
1546
|
+
missing.append(provider)
|
|
1547
|
+
|
|
1548
|
+
return configured, missing
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def _validate_startup_requirements() -> tuple[bool, str]:
|
|
1552
|
+
"""Validate that minimum requirements are met to run the server.
|
|
1553
|
+
|
|
1554
|
+
Returns:
|
|
1555
|
+
(ok, error_message) tuple. If ok is False, server should not start.
|
|
1556
|
+
"""
|
|
1557
|
+
configured, missing = _check_provider_keys()
|
|
1558
|
+
|
|
1559
|
+
if len(configured) == 0:
|
|
1560
|
+
return False, (
|
|
1561
|
+
"No API keys configured. Run 'second-opinion-mcp setup' to configure providers.\n\n"
|
|
1562
|
+
"Or manually configure keys:\n"
|
|
1563
|
+
" python -c \"import keyring; keyring.set_password('second-opinion', 'deepseek', 'YOUR_KEY')\"\n"
|
|
1564
|
+
" python -c \"import keyring; keyring.set_password('second-opinion', 'moonshot', 'YOUR_KEY')\"\n"
|
|
1565
|
+
)
|
|
1566
|
+
|
|
1567
|
+
if len(configured) == 1:
|
|
1568
|
+
return False, (
|
|
1569
|
+
f"Only 1 provider configured ({configured[0]}). At least 2 providers required.\n\n"
|
|
1570
|
+
"The 'consensus' tool requires multiple models to debate.\n"
|
|
1571
|
+
"Run 'second-opinion-mcp setup' to add another provider.\n\n"
|
|
1572
|
+
f"Missing: {', '.join(missing) if missing else 'none enabled'}\n"
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
return True, ""
|
|
1576
|
+
|
|
1577
|
+
|
|
1526
1578
|
def main():
|
|
1527
1579
|
"""Run the MCP server with stdio transport."""
|
|
1580
|
+
import sys
|
|
1581
|
+
|
|
1528
1582
|
# Configure logging when run directly (not when imported as library)
|
|
1529
1583
|
logging.basicConfig(
|
|
1530
1584
|
level=logging.INFO,
|
|
1531
1585
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
1532
1586
|
)
|
|
1587
|
+
|
|
1588
|
+
# Validate minimum requirements
|
|
1589
|
+
ok, error = _validate_startup_requirements()
|
|
1590
|
+
if not ok:
|
|
1591
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
1592
|
+
sys.exit(1)
|
|
1593
|
+
|
|
1594
|
+
# Log configured providers
|
|
1595
|
+
configured, _ = _check_provider_keys()
|
|
1596
|
+
logger.info("Starting with providers: %s", ", ".join(configured))
|
|
1597
|
+
|
|
1533
1598
|
mcp.run(transport="stdio")
|
|
1534
1599
|
|
|
1535
1600
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: second-opinion-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
second_opinion_mcp/__init__.py,sha256=THy3IptvV_KjNfRHMlhqUKNucORFcyKadnH8QfzFOXI,373
|
|
2
|
+
second_opinion_mcp/__main__.py,sha256=WDv19O6nvIH69GR_DUfXqDt-wm8bgsHNqvPm1LEIKzs,509
|
|
3
|
+
second_opinion_mcp/cli.py,sha256=A88Y0TZQ_B38YliHKzohAOLTZuGdMZzH4bEjyC1Tv_A,15279
|
|
4
|
+
second_opinion_mcp/server.py,sha256=Q6ra1wffKPoT2sfU6RIbTA-V93XgwA3faW3qLIVIoDw,60696
|
|
5
|
+
second_opinion_mcp-0.3.1.dist-info/METADATA,sha256=GKMM_Oh9_UgDLQrqzP1IXX-FlUP-8PNDRqA2yANeMCA,8476
|
|
6
|
+
second_opinion_mcp-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
second_opinion_mcp-0.3.1.dist-info/entry_points.txt,sha256=-E8BA2gFyU4qW-kJL8SV9Pg1Cc7glOCmraJoZH0PZP8,72
|
|
8
|
+
second_opinion_mcp-0.3.1.dist-info/licenses/LICENSE,sha256=dPx2Jy-Ejearvfh6IlF2PN4Srt-nZW8M4bW5EW7RPAg,1065
|
|
9
|
+
second_opinion_mcp-0.3.1.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
second_opinion_mcp/__init__.py,sha256=NpyIXDQRLwIAokC8JkOznUSIeX_sez7LjviT_Pg_ihM,373
|
|
2
|
-
second_opinion_mcp/__main__.py,sha256=WDv19O6nvIH69GR_DUfXqDt-wm8bgsHNqvPm1LEIKzs,509
|
|
3
|
-
second_opinion_mcp/cli.py,sha256=waLZH3Av817-3qqu3iG-GZ7YivF3Rp-7h9DzJ5YHgvo,8154
|
|
4
|
-
second_opinion_mcp/server.py,sha256=vD2Cl64PfblmDy1oqM1mwF5V8ouMSLFgzttymFq6UxA,58589
|
|
5
|
-
second_opinion_mcp-0.3.0.dist-info/METADATA,sha256=OqqHisGgVdSRtaHbsfkxlFc_JPsTahJSH7G6JbfZ6n4,8476
|
|
6
|
-
second_opinion_mcp-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
-
second_opinion_mcp-0.3.0.dist-info/entry_points.txt,sha256=-E8BA2gFyU4qW-kJL8SV9Pg1Cc7glOCmraJoZH0PZP8,72
|
|
8
|
-
second_opinion_mcp-0.3.0.dist-info/licenses/LICENSE,sha256=dPx2Jy-Ejearvfh6IlF2PN4Srt-nZW8M4bW5EW7RPAg,1065
|
|
9
|
-
second_opinion_mcp-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|