mcp-ollama-python 1.0.2__tar.gz → 1.0.3__tar.gz

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.
Files changed (25) hide show
  1. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/PKG-INFO +1 -1
  2. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/pyproject.toml +3 -1
  3. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/models.py +1 -0
  4. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/ollama_client.py +6 -3
  5. mcp_ollama_python-1.0.3/src/mcp_ollama_python/scripts/__init__.py +0 -0
  6. mcp_ollama_python-1.0.3/src/mcp_ollama_python/scripts/mcp_interactive.py +826 -0
  7. mcp_ollama_python-1.0.3/src/mcp_ollama_python/scripts/server_control.py +276 -0
  8. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/LICENSE +0 -0
  9. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/README.md +0 -0
  10. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/__init__.py +0 -0
  11. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/__main__.py +0 -0
  12. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/autoloader.py +0 -0
  13. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/main.py +0 -0
  14. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/response_formatter.py +0 -0
  15. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/server.py +0 -0
  16. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/__init__.py +0 -0
  17. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/chat.py +0 -0
  18. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/delete.py +0 -0
  19. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/embed.py +0 -0
  20. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/execute.py +0 -0
  21. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/generate.py +0 -0
  22. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/list.py +0 -0
  23. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/ps.py +0 -0
  24. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/pull.py +0 -0
  25. {mcp_ollama_python-1.0.2 → mcp_ollama_python-1.0.3}/src/mcp_ollama_python/tools/show.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-ollama-python
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Model Context Protocol server that proxies local Ollama to MCP clients like Windsurf and VS Code
5
5
  License-File: LICENSE
6
6
  Author: Pedja Blagojevic
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "mcp-ollama-python"
3
- version = "1.0.2"
3
+ version = "1.0.3"
4
4
  description = "Model Context Protocol server that proxies local Ollama to MCP clients like Windsurf and VS Code"
5
5
  authors = ["Pedja Blagojevic <pb@internetics.net>"]
6
6
  readme = "README.md"
@@ -17,6 +17,8 @@ tzdata = "^2024.1"
17
17
 
18
18
  [tool.poetry.scripts]
19
19
  mcp-ollama-python = "mcp_ollama_python.main:run"
20
+ mcp-interactive = "mcp_ollama_python.scripts.mcp_interactive:main"
21
+ mcp-server-control = "mcp_ollama_python.scripts.server_control:main"
20
22
 
21
23
  [tool.poetry.group.dev.dependencies]
22
24
  pytest = "^8.0.0"
@@ -4,6 +4,7 @@ Core types and enums for Ollama MCP Server
4
4
 
5
5
  from enum import Enum
6
6
  from typing import Any, Dict, List, Optional
7
+
7
8
  from pydantic import BaseModel, Field
8
9
 
9
10
 
@@ -3,9 +3,10 @@ Ollama HTTP client wrapper
3
3
  """
4
4
 
5
5
  import os
6
- import httpx
7
6
  from typing import Any, Dict, List, Optional, Union
8
7
 
8
+ import httpx
9
+
9
10
  try:
10
11
  from mcp_ollama_python.models import (
11
12
  GenerationOptions,
@@ -160,9 +161,11 @@ class OllamaClient:
160
161
  data["options"] = options.model_dump(exclude_unset=True)
161
162
  return await self._post("/api/chat", data)
162
163
 
163
- async def embed(self, model: str, input: Union[str, List[str]]) -> Dict[str, Any]:
164
+ async def embed(
165
+ self, model: str, input_text: Union[str, List[str]]
166
+ ) -> Dict[str, Any]:
164
167
  """Generate embeddings"""
165
- return await self._post("/api/embed", {"model": model, "input": input})
168
+ return await self._post("/api/embed", {"model": model, "input": input_text})
166
169
 
167
170
  async def ps(self) -> Dict[str, Any]:
168
171
  """List running models"""
@@ -0,0 +1,826 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Interactive MCP Server Management Script
4
+ Provides a menu-driven interface to manage and interact with the Ollama MCP Server
5
+
6
+ Package-compatible version — uses ~/.mcp-ollama-python/ for data storage.
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ import signal
14
+ import subprocess
15
+ import sys
16
+ import time
17
+ from typing import Dict, Optional
18
+
19
+ from mcp_ollama_python.ollama_client import OllamaClient
20
+ from mcp_ollama_python.server import OllamaMCPServer
21
+ import psutil
22
+
23
+ # Data directory in user home
24
+ DATA_DIR = Path.home() / ".mcp-ollama-python"
25
+ TMP_DIR = DATA_DIR / "tmp"
26
+ LOGS_DIR = DATA_DIR / "logs"
27
+ PID_FILE = TMP_DIR / ".mcp_ollama_server.pid"
28
+ ENV_VARS_FILE = TMP_DIR / ".mcp_env_vars.json"
29
+ LOG_FILE = LOGS_DIR / "mcp_ollama_server.log"
30
+ ERROR_LOG_FILE = LOGS_DIR / "mcp_ollama_server_error.log"
31
+
32
+
33
+ def _ensure_dirs():
34
+ """Create data directories on first use (not at import time)."""
35
+ TMP_DIR.mkdir(parents=True, exist_ok=True)
36
+ LOGS_DIR.mkdir(parents=True, exist_ok=True)
37
+
38
+
39
+ def is_mcp_server_process(pid: int) -> bool:
40
+ """Check if the given PID corresponds to an actual MCP server process"""
41
+ try:
42
+ process = psutil.Process(pid)
43
+ if not process.is_running():
44
+ return False
45
+
46
+ cmdline = process.cmdline()
47
+ if not cmdline:
48
+ return False
49
+
50
+ cmdline_str = " ".join(cmdline).lower()
51
+ is_python = "python" in cmdline_str or "python.exe" in cmdline_str
52
+ is_mcp = (
53
+ "mcp_ollama_python" in cmdline_str or "mcp-ollama-python" in cmdline_str
54
+ )
55
+ is_poetry_wrapper = "poetry" in cmdline_str and is_mcp
56
+
57
+ return (is_python and is_mcp) or is_poetry_wrapper
58
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
59
+ return False
60
+
61
+
62
+ def cleanup_stale_pipe_files(current_pid: Optional[int] = None):
63
+ """Remove all pipe files that don't correspond to the running MCP server"""
64
+ try:
65
+ for pipe_file in TMP_DIR.glob(".mcp_ollama_server_*.pipe"):
66
+ try:
67
+ filename = pipe_file.name
68
+ pid_str = filename.replace(".mcp_ollama_server_", "").replace(
69
+ ".pipe", ""
70
+ )
71
+ file_pid = int(pid_str)
72
+
73
+ if file_pid != current_pid or not is_mcp_server_process(file_pid):
74
+ try:
75
+ pipe_file.unlink()
76
+ except OSError:
77
+ pass
78
+ except (ValueError, OSError):
79
+ try:
80
+ pipe_file.unlink()
81
+ except OSError:
82
+ pass
83
+ except OSError:
84
+ pass
85
+
86
+
87
+ class MCPInteractive:
88
+ """Interactive MCP Server Manager"""
89
+
90
+ def __init__(self):
91
+ self.env_vars = self.load_env_vars()
92
+ self.server = None
93
+ self.ollama_client = None
94
+
95
+ def load_env_vars(self) -> Dict[str, str]:
96
+ """Load saved environment variables"""
97
+ if ENV_VARS_FILE.exists():
98
+ try:
99
+ return json.loads(ENV_VARS_FILE.read_text())
100
+ except (json.JSONDecodeError, OSError):
101
+ return {}
102
+ return {}
103
+
104
+ def save_env_vars(self):
105
+ """Save environment variables to file"""
106
+ ENV_VARS_FILE.write_text(json.dumps(self.env_vars, indent=2))
107
+
108
+ def apply_env_vars(self):
109
+ """Apply stored environment variables to current process"""
110
+ for key, value in self.env_vars.items():
111
+ os.environ[key] = value
112
+
113
+ def get_server_pid(self) -> Optional[int]:
114
+ """Get the PID of the running server if it exists and is valid"""
115
+ if PID_FILE.exists():
116
+ try:
117
+ pid = int(PID_FILE.read_text().strip())
118
+
119
+ if is_mcp_server_process(pid):
120
+ cleanup_stale_pipe_files(current_pid=pid)
121
+ return pid
122
+ else:
123
+ PID_FILE.unlink()
124
+ cleanup_stale_pipe_files()
125
+ return None
126
+ except (ValueError, FileNotFoundError):
127
+ return None
128
+
129
+ cleanup_stale_pipe_files()
130
+ return None
131
+
132
+ def check_server_status(self):
133
+ """Check and display server status"""
134
+ print("\n" + "=" * 60)
135
+ print("SERVER STATUS")
136
+ print("=" * 60)
137
+
138
+ pid = self.get_server_pid()
139
+ if pid:
140
+ print(f"✓ Server is RUNNING (PID: {pid})")
141
+ print(f" PID File: {PID_FILE}")
142
+
143
+ try:
144
+ process = psutil.Process(pid)
145
+ print(f" Process: {process.name()}")
146
+ print(f" Command: {' '.join(process.cmdline()[:3])}...")
147
+ except psutil.Error as e:
148
+ print(f" Debug error: {e}")
149
+ else:
150
+ print("✗ Server is NOT RUNNING")
151
+
152
+ if PID_FILE.exists():
153
+ try:
154
+ stored_pid = int(PID_FILE.read_text().strip())
155
+ print(f" Debug: PID file exists with PID {stored_pid}")
156
+ try:
157
+ process = psutil.Process(stored_pid)
158
+ cmdline = " ".join(process.cmdline())
159
+ print(" Debug: Process exists but validation failed")
160
+ print(f" Debug: Command line: {cmdline}")
161
+ except psutil.NoSuchProcess:
162
+ print(f" Debug: Process {stored_pid} does not exist")
163
+ except (ValueError, OSError) as e:
164
+ print(f" Debug: Error reading PID file: {e}")
165
+
166
+ # Check Ollama connection
167
+ print("\nOllama Connection:")
168
+ ollama_host = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434")
169
+ print(f" Host: {ollama_host}")
170
+
171
+ try:
172
+ import httpx
173
+
174
+ response = httpx.get(
175
+ f"{ollama_host}/api/tags", timeout=2.0, follow_redirects=True
176
+ )
177
+ if response.status_code == 200:
178
+ print(" Status: ✓ Connected")
179
+ data = response.json()
180
+ models = data.get("models", [])
181
+ print(f" Available Models: {len(models)}")
182
+ if models:
183
+ print(" Models:", ", ".join([m["name"] for m in models[:5]]))
184
+ if len(models) > 5:
185
+ print(f" ... and {len(models) - 5} more")
186
+ else:
187
+ print(f" Status: ✗ Error (HTTP {response.status_code})")
188
+ except httpx.RequestError as e:
189
+ print(f" Status: ✗ Cannot connect ({str(e)[:50]})")
190
+
191
+ print("=" * 60)
192
+ input("\nPress Enter to continue...")
193
+
194
+ def start_server(self):
195
+ """Start the MCP server"""
196
+ print("\n" + "=" * 60)
197
+ print("START SERVER")
198
+ print("=" * 60)
199
+
200
+ existing_pid = self.get_server_pid()
201
+ if existing_pid:
202
+ print("✗ Server is already running!")
203
+ print(f" PID: {existing_pid}")
204
+ input("\nPress Enter to continue...")
205
+ return
206
+
207
+ print("\nCleaning up stale files...")
208
+ cleanup_stale_pipe_files()
209
+
210
+ print("Applying environment variables...")
211
+ self.apply_env_vars()
212
+
213
+ print("Starting Ollama MCP Server...")
214
+
215
+ try:
216
+ env = os.environ.copy()
217
+ env.update(self.env_vars)
218
+
219
+ # Remove PYTHONHOME which can interfere
220
+ env.pop("PYTHONHOME", None)
221
+
222
+ # Clear PYTHONPATH to prevent conflicts
223
+ if "PYTHONPATH" in env:
224
+ print(f" Clearing PYTHONPATH: {env['PYTHONPATH']}")
225
+ env.pop("PYTHONPATH", None)
226
+
227
+ # Open log files
228
+ log_file = open(LOG_FILE, "w", encoding="utf-8")
229
+ error_log_file = open(ERROR_LOG_FILE, "w", encoding="utf-8")
230
+
231
+ # Create a pipe for stdin to keep the server running
232
+ stdin_read, stdin_write = os.pipe()
233
+
234
+ try:
235
+ # Windows-specific flags
236
+ creationflags = 0
237
+ if sys.platform == "win32":
238
+ creationflags = (
239
+ subprocess.CREATE_NEW_PROCESS_GROUP
240
+ | subprocess.CREATE_NO_WINDOW
241
+ )
242
+
243
+ # Use the current Python interpreter
244
+ python_exe = sys.executable
245
+ print(f" Using Python: {python_exe}")
246
+ print(f" Command: {python_exe} -m mcp_ollama_python")
247
+
248
+ process = subprocess.Popen(
249
+ [python_exe, "-m", "mcp_ollama_python"],
250
+ stdin=stdin_read,
251
+ stdout=log_file,
252
+ stderr=error_log_file,
253
+ env=env,
254
+ start_new_session=True,
255
+ close_fds=False,
256
+ creationflags=creationflags,
257
+ )
258
+ except Exception:
259
+ # Clean up all resources on Popen failure
260
+ os.close(stdin_read)
261
+ os.close(stdin_write)
262
+ log_file.close()
263
+ error_log_file.close()
264
+ raise
265
+
266
+ # Close the read end in parent process (child has it)
267
+ os.close(stdin_read)
268
+ # Log files are inherited by the child; close parent copies
269
+ log_file.close()
270
+ error_log_file.close()
271
+ # Store write end so we can close it when stopping
272
+ pipe_file = TMP_DIR / f".mcp_ollama_server_{process.pid}.pipe"
273
+ pipe_file.write_text(str(stdin_write))
274
+
275
+ PID_FILE.write_text(str(process.pid))
276
+ time.sleep(1)
277
+
278
+ if process.poll() is None:
279
+ print(f"\n✓ Server started successfully (PID: {process.pid})")
280
+ print(f" Log file: {LOG_FILE}")
281
+ print(f" Error log: {ERROR_LOG_FILE}")
282
+ else:
283
+ print("\n✗ Server failed to start")
284
+ if ERROR_LOG_FILE.exists():
285
+ error_content = ERROR_LOG_FILE.read_text()
286
+ if error_content:
287
+ print(f"Error: {error_content[:500]}")
288
+ except (OSError, subprocess.SubprocessError) as e:
289
+ print(f"\n✗ Failed to start server: {e}")
290
+
291
+ input("\nPress Enter to continue...")
292
+
293
+ def stop_server(self):
294
+ """Stop the running server"""
295
+ print("\n" + "=" * 60)
296
+ print("STOP SERVER")
297
+ print("=" * 60)
298
+
299
+ pid = self.get_server_pid()
300
+
301
+ if not pid:
302
+ print("✗ No server is currently running")
303
+ input("\nPress Enter to continue...")
304
+ return
305
+
306
+ print(f"\nStopping server (PID: {pid})...")
307
+
308
+ try:
309
+ # Remove the pipe file to signal EOF to the child process.
310
+ # Note: the numeric FD stored in the file is only valid in the
311
+ # process that created it, so we just delete the file here.
312
+ pipe_file = TMP_DIR / f".mcp_ollama_server_{pid}.pipe"
313
+ if pipe_file.exists():
314
+ try:
315
+ pipe_file.unlink()
316
+ print(" Removed pipe file")
317
+ except OSError:
318
+ pass
319
+
320
+ os.kill(pid, signal.SIGTERM)
321
+
322
+ for _ in range(50):
323
+ try:
324
+ os.kill(pid, 0)
325
+ time.sleep(0.1)
326
+ except OSError:
327
+ break
328
+
329
+ try:
330
+ os.kill(pid, 0)
331
+ print(" Server didn't stop gracefully, forcing shutdown...")
332
+ try:
333
+ process = psutil.Process(pid)
334
+ if sys.platform == "win32":
335
+ process.terminate()
336
+ else:
337
+ process.kill()
338
+ process.wait(timeout=2)
339
+ except (psutil.NoSuchProcess, psutil.TimeoutExpired):
340
+ pass
341
+ except OSError:
342
+ pass
343
+
344
+ if PID_FILE.exists():
345
+ PID_FILE.unlink()
346
+
347
+ print(" Cleaning up temporary files...")
348
+ cleanup_stale_pipe_files()
349
+
350
+ print("\n✓ Server stopped successfully")
351
+ except (OSError, psutil.Error) as e:
352
+ print(f"\n✗ Failed to stop server: {e}")
353
+ if PID_FILE.exists():
354
+ PID_FILE.unlink()
355
+ cleanup_stale_pipe_files()
356
+
357
+ input("\nPress Enter to continue...")
358
+
359
+ def list_commands(self):
360
+ """List available MCP commands"""
361
+ print("\n" + "=" * 60)
362
+ print("AVAILABLE MCP COMMANDS")
363
+ print("=" * 60)
364
+
365
+ print("\nInitializing server to discover tools...")
366
+
367
+ try:
368
+ from mcp_ollama_python.autoloader import discover_tools_with_handlers
369
+
370
+ async def get_tools():
371
+ registry = await discover_tools_with_handlers()
372
+ return registry.tools
373
+
374
+ tools = asyncio.run(get_tools())
375
+
376
+ print(f"\nFound {len(tools)} tools:\n")
377
+
378
+ for i, tool in enumerate(tools, 1):
379
+ print(f"{i}. {tool.name}")
380
+ print(f" Description: {tool.description}")
381
+
382
+ if tool.input_schema and "properties" in tool.input_schema:
383
+ props = tool.input_schema["properties"]
384
+ required = tool.input_schema.get("required", [])
385
+
386
+ print(" Arguments:")
387
+ for prop_name, prop_info in props.items():
388
+ req_marker = "*" if prop_name in required else " "
389
+ prop_type = prop_info.get("type", "any")
390
+ prop_desc = prop_info.get("description", "No description")
391
+ print(
392
+ f" {req_marker} {prop_name} ({prop_type}): {prop_desc}"
393
+ )
394
+
395
+ print()
396
+
397
+ except (ImportError, RuntimeError) as e:
398
+ print(f"\n✗ Error discovering tools: {e}")
399
+ import traceback
400
+
401
+ traceback.print_exc()
402
+
403
+ input("\nPress Enter to continue...")
404
+
405
+ def manage_env_vars(self):
406
+ """Manage environment variables"""
407
+ while True:
408
+ print("\n" + "=" * 60)
409
+ print("ENVIRONMENT VARIABLES MANAGEMENT")
410
+ print("=" * 60)
411
+
412
+ print("\n1. View current environment variables")
413
+ print("2. Add/Update environment variable")
414
+ print("3. Remove environment variable")
415
+ print("4. Reset to defaults")
416
+ print("5. Back to main menu")
417
+
418
+ choice = input("\nSelect option (1-5): ").strip()
419
+
420
+ if choice == "1":
421
+ self.view_env_vars()
422
+ elif choice == "2":
423
+ self.add_env_var()
424
+ elif choice == "3":
425
+ self.remove_env_var()
426
+ elif choice == "4":
427
+ self.reset_env_vars()
428
+ elif choice == "5":
429
+ break
430
+ else:
431
+ print("Invalid option. Please try again.")
432
+
433
+ def view_env_vars(self):
434
+ """View current environment variables"""
435
+ print("\n" + "-" * 60)
436
+ print("CURRENT ENVIRONMENT VARIABLES")
437
+ print("-" * 60)
438
+
439
+ if not self.env_vars:
440
+ print("\nNo custom environment variables set.")
441
+ print("\nCommon variables you might want to set:")
442
+ print(" OLLAMA_HOST - Ollama server URL (default: http://127.0.0.1:11434)")
443
+ print(" OLLAMA_API_KEY - API key for Ollama (if required)")
444
+ print(" OLLAMA_MODELS - Custom models directory")
445
+ else:
446
+ print()
447
+ for key, value in self.env_vars.items():
448
+ print(f" {key} = {value}")
449
+
450
+ print("\n" + "-" * 60)
451
+ print("SYSTEM ENVIRONMENT VARIABLES (Ollama-related)")
452
+ print("-" * 60)
453
+
454
+ ollama_vars = {k: v for k, v in os.environ.items() if "OLLAMA" in k.upper()}
455
+ if ollama_vars:
456
+ for key, value in ollama_vars.items():
457
+ print(f" {key} = {value}")
458
+ else:
459
+ print("\nNo Ollama-related system variables found.")
460
+
461
+ input("\nPress Enter to continue...")
462
+
463
+ def add_env_var(self):
464
+ """Add or update an environment variable"""
465
+ print("\n" + "-" * 60)
466
+ print("ADD/UPDATE ENVIRONMENT VARIABLE")
467
+ print("-" * 60)
468
+
469
+ print("\nCommon variables:")
470
+ print(" OLLAMA_HOST")
471
+ print(" OLLAMA_API_KEY")
472
+ print(" OLLAMA_MODELS")
473
+
474
+ key = input("\nEnter variable name (or 'cancel' to go back): ").strip()
475
+
476
+ if key.lower() == "cancel":
477
+ return
478
+
479
+ if not key:
480
+ print("Variable name cannot be empty.")
481
+ input("\nPress Enter to continue...")
482
+ return
483
+
484
+ current = self.env_vars.get(key, os.environ.get(key, ""))
485
+ if current:
486
+ print(f"\nCurrent value: {current}")
487
+
488
+ value = input(f"Enter value for {key}: ").strip()
489
+
490
+ if value:
491
+ self.env_vars[key] = value
492
+ self.save_env_vars()
493
+ print(f"\n✓ Set {key} = {value}")
494
+ else:
495
+ print("\nValue cannot be empty. Variable not updated.")
496
+
497
+ input("\nPress Enter to continue...")
498
+
499
+ def remove_env_var(self):
500
+ """Remove an environment variable"""
501
+ print("\n" + "-" * 60)
502
+ print("REMOVE ENVIRONMENT VARIABLE")
503
+ print("-" * 60)
504
+
505
+ if not self.env_vars:
506
+ print("\nNo custom environment variables to remove.")
507
+ input("\nPress Enter to continue...")
508
+ return
509
+
510
+ print("\nCurrent variables:")
511
+ for i, key in enumerate(self.env_vars.keys(), 1):
512
+ print(f" {i}. {key} = {self.env_vars[key]}")
513
+
514
+ choice = input(
515
+ "\nEnter variable name or number to remove (or 'cancel'): "
516
+ ).strip()
517
+
518
+ if choice.lower() == "cancel":
519
+ return
520
+
521
+ try:
522
+ idx = int(choice) - 1
523
+ keys = list(self.env_vars.keys())
524
+ if 0 <= idx < len(keys):
525
+ key = keys[idx]
526
+ else:
527
+ print("Invalid number.")
528
+ input("\nPress Enter to continue...")
529
+ return
530
+ except ValueError:
531
+ key = choice
532
+
533
+ if key in self.env_vars:
534
+ del self.env_vars[key]
535
+ self.save_env_vars()
536
+ print(f"\n✓ Removed {key}")
537
+ else:
538
+ print(f"\n✗ Variable '{key}' not found.")
539
+
540
+ input("\nPress Enter to continue...")
541
+
542
+ def reset_env_vars(self):
543
+ """Reset environment variables to defaults"""
544
+ print("\n" + "-" * 60)
545
+ print("RESET ENVIRONMENT VARIABLES")
546
+ print("-" * 60)
547
+
548
+ confirm = (
549
+ input("\nAre you sure you want to reset all custom variables? (yes/no): ")
550
+ .strip()
551
+ .lower()
552
+ )
553
+
554
+ if confirm == "yes":
555
+ self.env_vars = {}
556
+ self.save_env_vars()
557
+ print("\n✓ All custom environment variables cleared.")
558
+ else:
559
+ print("\nReset cancelled.")
560
+
561
+ input("\nPress Enter to continue...")
562
+
563
+ def run_mcp_command(self):
564
+ """Run an MCP command interactively"""
565
+ print("\n" + "=" * 60)
566
+ print("RUN MCP COMMAND")
567
+ print("=" * 60)
568
+
569
+ print("\nInitializing MCP server...")
570
+
571
+ try:
572
+ self.apply_env_vars()
573
+
574
+ async def execute_command():
575
+ ollama_client = OllamaClient()
576
+ server = OllamaMCPServer(ollama_client)
577
+
578
+ tools_result = await server.handle_list_tools()
579
+ tools = tools_result["tools"]
580
+
581
+ if not tools:
582
+ print("\n✗ No tools available.")
583
+ return
584
+
585
+ print(f"\nAvailable commands ({len(tools)}):\n")
586
+ for i, tool in enumerate(tools, 1):
587
+ print(f"{i}. {tool['name']}")
588
+ print(f" {tool['description']}")
589
+
590
+ print()
591
+ choice = input(
592
+ "Select command number (or 'cancel' to go back): "
593
+ ).strip()
594
+
595
+ if choice.lower() == "cancel":
596
+ return
597
+
598
+ try:
599
+ idx = int(choice) - 1
600
+ if not (0 <= idx < len(tools)):
601
+ print("\n✗ Invalid selection.")
602
+ return
603
+ except ValueError:
604
+ print("\n✗ Invalid input. Please enter a number.")
605
+ return
606
+
607
+ selected_tool = tools[idx]
608
+ tool_name = selected_tool["name"]
609
+
610
+ print("\n" + "-" * 60)
611
+ print(f"COMMAND: {tool_name}")
612
+ print("-" * 60)
613
+ print(f"Description: {selected_tool['description']}")
614
+
615
+ args = {}
616
+ schema = selected_tool.get("inputSchema", {})
617
+ properties = schema.get("properties", {})
618
+ required = schema.get("required", [])
619
+
620
+ if properties:
621
+ print("\nArguments:")
622
+ for prop_name, prop_info in properties.items():
623
+ is_required = prop_name in required
624
+ prop_type = prop_info.get("type", "string")
625
+ prop_desc = prop_info.get("description", "")
626
+
627
+ req_marker = "[REQUIRED]" if is_required else "[OPTIONAL]"
628
+ print(f"\n {prop_name} ({prop_type}) {req_marker}")
629
+ if prop_desc:
630
+ print(f" {prop_desc}")
631
+
632
+ if prop_type == "array":
633
+ if prop_name == "messages" and tool_name == "ollama_chat":
634
+ value = input(" Enter your message: ").strip()
635
+ if value:
636
+ args[prop_name] = [
637
+ {"role": "user", "content": value}
638
+ ]
639
+ elif is_required:
640
+ print(f" ✗ {prop_name} is required!")
641
+ return
642
+ else:
643
+ value = input(
644
+ " Enter value (comma-separated for array): "
645
+ ).strip()
646
+ if value:
647
+ args[prop_name] = [
648
+ v.strip() for v in value.split(",")
649
+ ]
650
+ elif is_required:
651
+ print(f" ✗ {prop_name} is required!")
652
+ return
653
+ elif prop_type == "object":
654
+ value = input(" Enter value (JSON format): ").strip()
655
+ if value:
656
+ try:
657
+ args[prop_name] = json.loads(value)
658
+ except json.JSONDecodeError:
659
+ print(" ✗ Invalid JSON format!")
660
+ return
661
+ elif is_required:
662
+ print(f" ✗ {prop_name} is required!")
663
+ return
664
+ else:
665
+ value = input(" Enter value: ").strip()
666
+ if value:
667
+ args[prop_name] = value
668
+ elif is_required:
669
+ print(f" ✗ {prop_name} is required!")
670
+ return
671
+
672
+ print("\nOutput format:")
673
+ print(" 1. JSON")
674
+ print(" 2. Markdown")
675
+ format_choice = (
676
+ input("Select format (1 or 2, default: 1): ").strip() or "1"
677
+ )
678
+ args["format"] = "markdown" if format_choice == "2" else "json"
679
+
680
+ print("\n" + "=" * 60)
681
+ print("EXECUTING COMMAND...")
682
+ print("=" * 60)
683
+
684
+ result = await server.handle_call_tool(tool_name, args)
685
+
686
+ print("\n" + "-" * 60)
687
+ print("RESULT:")
688
+ print("-" * 60)
689
+
690
+ if "content" in result:
691
+ for item in result["content"]:
692
+ if item.get("type") == "text":
693
+ print(item["text"])
694
+
695
+ if result.get("isError"):
696
+ print("\n✗ Command execution failed.")
697
+ else:
698
+ print("\n✓ Command executed successfully.")
699
+
700
+ await ollama_client.client.aclose()
701
+
702
+ asyncio.run(execute_command())
703
+
704
+ except (RuntimeError, asyncio.CancelledError) as e:
705
+ print(f"\n✗ Error executing command: {e}")
706
+ import traceback
707
+
708
+ traceback.print_exc()
709
+
710
+ input("\nPress Enter to continue...")
711
+
712
+ def view_logs(self):
713
+ """View server logs"""
714
+ print("\n" + "=" * 60)
715
+ print("SERVER LOGS")
716
+ print("=" * 60)
717
+
718
+ if LOG_FILE.exists():
719
+ print("\nLog file:")
720
+ print(LOG_FILE)
721
+ try:
722
+ with open(LOG_FILE, "r", encoding="utf-8", errors="replace") as f:
723
+ content = f.read()
724
+ if content.strip():
725
+ print("\nLog content:")
726
+ print(content)
727
+ else:
728
+ print("\nLog file is empty.")
729
+ except OSError as e:
730
+ print(f"\nError reading log file: {e}")
731
+ else:
732
+ print("\nNo log file found.")
733
+
734
+ if ERROR_LOG_FILE.exists():
735
+ print("\n" + "-" * 60)
736
+ print("Error log file:")
737
+ print(ERROR_LOG_FILE)
738
+ try:
739
+ with open(ERROR_LOG_FILE, "r", encoding="utf-8", errors="replace") as f:
740
+ content = f.read()
741
+ if content.strip():
742
+ print("\nError log content:")
743
+ print(content)
744
+ else:
745
+ print("\nError log file is empty (no errors).")
746
+ except OSError as e:
747
+ print(f"\nError reading error log file: {e}")
748
+ else:
749
+ print("\nNo error log file found.")
750
+
751
+ print("\n" + "-" * 60)
752
+ print("File Information:")
753
+ if LOG_FILE.exists():
754
+ size = LOG_FILE.stat().st_size
755
+ print(f" Log file size: {size} bytes")
756
+ if ERROR_LOG_FILE.exists():
757
+ size = ERROR_LOG_FILE.stat().st_size
758
+ print(f" Error log file size: {size} bytes")
759
+
760
+ input("\nPress Enter to continue...")
761
+
762
+ def show_menu(self):
763
+ """Display main menu"""
764
+ print("\n" + "=" * 60)
765
+ print("OLLAMA MCP SERVER - INTERACTIVE MANAGER")
766
+ print("=" * 60)
767
+ print("1. Check MCP server status")
768
+ print("2. Start server")
769
+ print("3. Stop server")
770
+ print("4. View server logs")
771
+ print("5. List server commands and arguments")
772
+ print("6. Manage environment variables")
773
+ print("7. View current environment variables")
774
+ print("8. Run MCP command")
775
+ print("9. Exit")
776
+ print("\n" + "=" * 60)
777
+
778
+ def run(self):
779
+ """Main loop"""
780
+ while True:
781
+ self.show_menu()
782
+ choice = input("\nSelect option (1-9): ").strip()
783
+
784
+ if choice == "1":
785
+ self.check_server_status()
786
+ elif choice == "2":
787
+ self.start_server()
788
+ elif choice == "3":
789
+ self.stop_server()
790
+ elif choice == "4":
791
+ self.view_logs()
792
+ elif choice == "5":
793
+ self.list_commands()
794
+ elif choice == "6":
795
+ self.manage_env_vars()
796
+ elif choice == "7":
797
+ self.view_env_vars()
798
+ elif choice == "8":
799
+ self.run_mcp_command()
800
+ elif choice == "9":
801
+ print("\nExiting... Goodbye!")
802
+ break
803
+ else:
804
+ print("\nInvalid option. Please try again.")
805
+ input("\nPress Enter to continue...")
806
+
807
+
808
+ def main():
809
+ """Main entry point"""
810
+ _ensure_dirs()
811
+ try:
812
+ manager = MCPInteractive()
813
+ manager.run()
814
+ except KeyboardInterrupt:
815
+ print("\n\nInterrupted by user. Exiting...")
816
+ sys.exit(0)
817
+ except (RuntimeError, OSError) as e:
818
+ print(f"\nFatal error: {e}")
819
+ import traceback
820
+
821
+ traceback.print_exc()
822
+ sys.exit(1)
823
+
824
+
825
+ if __name__ == "__main__":
826
+ main()
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Ollama MCP Server Control Script
4
+ Provides easy start/stop/status commands for the MCP server
5
+
6
+ Package-compatible version — uses ~/.mcp-ollama-python/ for data storage.
7
+ """
8
+
9
+ import os
10
+ import signal
11
+ import subprocess
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ import psutil
18
+
19
+ # Data directory in user home
20
+ DATA_DIR = Path.home() / ".mcp-ollama-python"
21
+ TMP_DIR = DATA_DIR / "tmp"
22
+ PID_FILE = TMP_DIR / ".mcp_ollama_server.pid"
23
+
24
+
25
+ def _ensure_dirs():
26
+ """Create data directories on first use (not at import time)."""
27
+ TMP_DIR.mkdir(parents=True, exist_ok=True)
28
+
29
+
30
+ def is_mcp_server_process(pid: int) -> bool:
31
+ """Check if the given PID corresponds to an actual MCP server process"""
32
+ try:
33
+ process = psutil.Process(pid)
34
+ if not process.is_running():
35
+ return False
36
+
37
+ cmdline = process.cmdline()
38
+ if not cmdline:
39
+ return False
40
+
41
+ cmdline_str = " ".join(cmdline).lower()
42
+ return "python" in cmdline_str and "mcp_ollama_python" in cmdline_str
43
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
44
+ return False
45
+
46
+
47
+ def cleanup_stale_pipe_files(current_pid: Optional[int] = None):
48
+ """Remove all pipe files that don't correspond to the running MCP server"""
49
+ try:
50
+ for pipe_file in TMP_DIR.glob(".mcp_ollama_server_*.pipe"):
51
+ try:
52
+ filename = pipe_file.name
53
+ pid_str = filename.replace(".mcp_ollama_server_", "").replace(
54
+ ".pipe", ""
55
+ )
56
+ file_pid = int(pid_str)
57
+
58
+ if file_pid != current_pid or not is_mcp_server_process(file_pid):
59
+ try:
60
+ pipe_file.unlink()
61
+ print(f" Cleaned up stale pipe file: {pipe_file.name}")
62
+ except OSError as e:
63
+ print(f" Warning: Could not remove {pipe_file.name}: {e}")
64
+ except (ValueError, OSError):
65
+ try:
66
+ pipe_file.unlink()
67
+ print(f" Cleaned up invalid pipe file: {pipe_file.name}")
68
+ except OSError:
69
+ pass
70
+ except OSError as e:
71
+ print(f" Warning: Error during pipe cleanup: {e}")
72
+
73
+
74
+ def get_server_pid() -> Optional[int]:
75
+ """Get the PID of the running server if it exists and is valid"""
76
+ if PID_FILE.exists():
77
+ try:
78
+ pid = int(PID_FILE.read_text().strip())
79
+
80
+ if is_mcp_server_process(pid):
81
+ cleanup_stale_pipe_files(current_pid=pid)
82
+ return pid
83
+ else:
84
+ print(" Found stale PID file, cleaning up...")
85
+ PID_FILE.unlink()
86
+ cleanup_stale_pipe_files()
87
+ return None
88
+ except (ValueError, FileNotFoundError):
89
+ return None
90
+
91
+ cleanup_stale_pipe_files()
92
+ return None
93
+
94
+
95
+ def start_server():
96
+ """Start the MCP server"""
97
+ existing_pid = get_server_pid()
98
+ if existing_pid:
99
+ print("Server is already running!")
100
+ print(f"PID: {existing_pid}")
101
+ return 1
102
+
103
+ print("Starting Ollama MCP Server...")
104
+ print(" Cleaning up stale files...")
105
+ cleanup_stale_pipe_files()
106
+
107
+ try:
108
+ process = subprocess.Popen(
109
+ [sys.executable, "-m", "mcp_ollama_python"],
110
+ stdout=subprocess.PIPE,
111
+ stderr=subprocess.PIPE,
112
+ start_new_session=True,
113
+ )
114
+
115
+ PID_FILE.write_text(str(process.pid))
116
+
117
+ time.sleep(1)
118
+
119
+ if process.poll() is None:
120
+ print(f"✓ Server started successfully (PID: {process.pid})")
121
+ print(f" PID file: {PID_FILE}")
122
+ print("Use 'mcp-server-control stop' to stop the server")
123
+ return 0
124
+ else:
125
+ _, stderr = process.communicate()
126
+ print("✗ Server failed to start")
127
+ if stderr:
128
+ print(f"Error: {stderr.decode()}")
129
+ if PID_FILE.exists():
130
+ PID_FILE.unlink()
131
+ return 1
132
+
133
+ except (OSError, subprocess.SubprocessError) as e:
134
+ print(f"✗ Failed to start server: {e}")
135
+ if PID_FILE.exists():
136
+ PID_FILE.unlink()
137
+ return 1
138
+
139
+
140
+ def stop_server():
141
+ """Stop the running server"""
142
+ pid = get_server_pid()
143
+
144
+ if not pid:
145
+ print("No server is currently running")
146
+ print(" Cleaning up stale files...")
147
+ cleanup_stale_pipe_files()
148
+ return 1
149
+
150
+ print(f"Stopping server (PID: {pid})...")
151
+
152
+ try:
153
+ # Remove the pipe file to signal EOF to the child process.
154
+ # Note: the numeric FD stored in the file is only valid in the
155
+ # process that created it, so we just delete the file here.
156
+ pipe_file = TMP_DIR / f".mcp_ollama_server_{pid}.pipe"
157
+ if pipe_file.exists():
158
+ try:
159
+ pipe_file.unlink()
160
+ print(" Removed pipe file")
161
+ except OSError:
162
+ pass
163
+
164
+ os.kill(pid, signal.SIGTERM)
165
+
166
+ for _ in range(50):
167
+ try:
168
+ os.kill(pid, 0)
169
+ time.sleep(0.1)
170
+ except OSError:
171
+ break
172
+
173
+ try:
174
+ os.kill(pid, 0)
175
+ print(" Server didn't stop gracefully, forcing shutdown...")
176
+ try:
177
+ process = psutil.Process(pid)
178
+ if sys.platform == "win32":
179
+ process.terminate()
180
+ else:
181
+ process.kill() # Sends SIGKILL on Unix
182
+ process.wait(timeout=2)
183
+ except (psutil.NoSuchProcess, psutil.TimeoutExpired):
184
+ pass
185
+ except OSError:
186
+ pass
187
+
188
+ if PID_FILE.exists():
189
+ PID_FILE.unlink()
190
+
191
+ print(" Cleaning up temporary files...")
192
+ cleanup_stale_pipe_files()
193
+
194
+ print("✓ Server stopped successfully")
195
+ return 0
196
+
197
+ except (OSError, psutil.Error) as e:
198
+ print(f"✗ Failed to stop server: {e}")
199
+ if PID_FILE.exists():
200
+ PID_FILE.unlink()
201
+ cleanup_stale_pipe_files()
202
+ return 1
203
+
204
+
205
+ def restart_server():
206
+ """Restart the server"""
207
+ print("Restarting server...")
208
+ stop_server()
209
+ time.sleep(1)
210
+ return start_server()
211
+
212
+
213
+ def server_status():
214
+ """Check server status"""
215
+ pid = get_server_pid()
216
+
217
+ if pid:
218
+ print(f"✓ Server is running (PID: {pid})")
219
+ return 0
220
+ else:
221
+ print("✗ Server is not running")
222
+ return 1
223
+
224
+
225
+ def show_help():
226
+ """Show help message"""
227
+ print(
228
+ """
229
+ Ollama MCP Server Control
230
+
231
+ Usage:
232
+ mcp-server-control [command]
233
+
234
+ Commands:
235
+ start Start the MCP server
236
+ stop Stop the running server
237
+ restart Restart the server
238
+ status Check if server is running
239
+ help Show this help message
240
+
241
+ Examples:
242
+ mcp-server-control start
243
+ mcp-server-control stop
244
+ mcp-server-control status
245
+ """
246
+ )
247
+ return 0
248
+
249
+
250
+ def main():
251
+ """Main entry point"""
252
+ _ensure_dirs()
253
+ if len(sys.argv) < 2:
254
+ show_help()
255
+ return 1
256
+
257
+ command = sys.argv[1].lower()
258
+
259
+ commands = {
260
+ "start": start_server,
261
+ "stop": stop_server,
262
+ "restart": restart_server,
263
+ "status": server_status,
264
+ "help": show_help,
265
+ }
266
+
267
+ if command in commands:
268
+ return commands[command]()
269
+ else:
270
+ print(f"Unknown command: {command}")
271
+ show_help()
272
+ return 1
273
+
274
+
275
+ if __name__ == "__main__":
276
+ sys.exit(main())