sokrates-mcp 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sokrates_mcp/main.py CHANGED
@@ -294,6 +294,43 @@ async def generate_code_review(
294
294
  ) -> str:
295
295
  return await workflow.generate_code_review(ctx=ctx, provider=provider, model=model, review_type=review_type, source_directory=source_directory, source_file_paths=source_file_paths, target_directory=target_directory)
296
296
 
297
+ @mcp.tool(
298
+ name="read_from_file",
299
+ description="Read a file from the local disk at the given file path and return it's contents.",
300
+ tags={"file","read","load","local"}
301
+ )
302
+ async def read_from_file(
303
+ ctx: Context,
304
+ file_path: Annotated[str, Field(description="The source file path to use for reading the file. This should be an absolute file path on the disk.")],
305
+ ) -> str:
306
+ return await workflow.read_from_file(ctx=ctx, file_path=file_path)
307
+
308
+ @mcp.tool(
309
+ name="store_to_file",
310
+ description="Store a file with the provided content to the local drive at the provided file path.",
311
+ tags={"file","store","save","local"}
312
+ )
313
+ async def store_to_file(
314
+ ctx: Context,
315
+ file_path: Annotated[str, Field(description="The target file path to use for storing the file. This should be an absolute file path on the disk.")],
316
+ file_content: Annotated[str, Field(description="The content that should be written to the target file.")],
317
+ ) -> str:
318
+ return await workflow.store_to_file(ctx=ctx, file_path=file_path, file_content=file_content)
319
+
320
+ @mcp.tool(
321
+ name="roll_dice",
322
+ description="Rolls the given number of dice with the specified number of sides for the given number of times and returns the result. For example you can also instruct to throw a W12, which should then set the side_count to 12.",
323
+ tags={"dice","roll","random"}
324
+ )
325
+ async def roll_dice(
326
+ ctx: Context,
327
+ number_of_dice: Annotated[int, Field(description="The number of dice to to use for rolling.", default=1)],
328
+ side_count: Annotated[int, Field(description="The number of sides of the dice to use for rolling.", default=6)],
329
+ number_of_rolls: Annotated[int, Field(description="The count of dice rolls to execute.", default=1)]
330
+ ) -> str:
331
+ return await workflow.roll_dice(ctx=ctx, number_of_dice=number_of_dice, side_count=side_count, number_of_rolls=number_of_rolls)
332
+
333
+
297
334
  @mcp.tool(
298
335
  name="list_available_models_for_provider",
299
336
  description="Lists all available large language models and the target api endpoint configured as provider for the sokrates-mcp server.",
@@ -18,6 +18,7 @@ import logging
18
18
  from urllib.parse import urlparse
19
19
  from pathlib import Path
20
20
  from sokrates import Config
21
+ from typing import Dict, List, Optional, Any
21
22
 
22
23
  DEFAULT_API_ENDPOINT = "http://localhost:1234/v1"
23
24
  DEFAULT_API_KEY = "mykey"
@@ -53,7 +54,7 @@ class MCPConfig:
53
54
  "openai"
54
55
  ]
55
56
 
56
- def __init__(self, config_file_path=CONFIG_FILE_PATH, api_endpoint = DEFAULT_API_ENDPOINT, api_key = DEFAULT_API_KEY, model= DEFAULT_MODEL, verbose=False):
57
+ def __init__(self, config_file_path: str = CONFIG_FILE_PATH, api_endpoint: str = DEFAULT_API_ENDPOINT, api_key: str = DEFAULT_API_KEY, model: str = DEFAULT_MODEL, verbose: bool = False):
57
58
  """Initialize MCP configuration.
58
59
 
59
60
  Args:
@@ -64,15 +65,15 @@ class MCPConfig:
64
65
  model (str): Model name to use. Defaults to DEFAULT_MODEL.
65
66
  verbose (bool): Enable verbose logging. Defaults to False.
66
67
 
67
- Returns:
68
- None
69
-
70
68
  Side Effects:
71
69
  Initializes instance attributes with values from config file or defaults
72
70
  Sets up logging based on verbose parameter
73
71
  """
74
72
  self.logger = logging.getLogger(__name__)
75
73
  self.config_file_path = config_file_path
74
+ # Validate config file path
75
+ if not self._validate_config_file_path(config_file_path):
76
+ raise ValueError(f"Invalid config file path: {config_file_path}")
76
77
  config_data = self._load_config_from_file(self.config_file_path)
77
78
 
78
79
  prompts_directory = config_data.get("prompts_directory", self.DEFAULT_PROMPTS_DIRECTORY)
@@ -80,14 +81,13 @@ class MCPConfig:
80
81
  raise ValueError(f"Invalid prompts directory: {prompts_directory}")
81
82
  self.prompts_directory = prompts_directory
82
83
 
84
+ # Validate prompt files using helper method
83
85
  refinement_prompt_filename = config_data.get("refinement_prompt_filename", self.DEFAULT_REFINEMENT_PROMPT_FILENAME)
84
- if not os.path.exists(os.path.join(prompts_directory, refinement_prompt_filename)):
85
- raise FileNotFoundError(f"Refinement prompt file not found: {refinement_prompt_filename}")
86
+ self._validate_prompt_file_exists(prompts_directory, refinement_prompt_filename)
86
87
  self.refinement_prompt_filename = refinement_prompt_filename
87
88
 
88
89
  refinement_coding_prompt_filename = config_data.get("refinement_coding_prompt_filename", self.DEFAULT_REFINEMENT_CODING_PROMPT_FILENAME)
89
- if not os.path.exists(os.path.join(prompts_directory, refinement_coding_prompt_filename)):
90
- raise FileNotFoundError(f"Refinement coding prompt file not found: {refinement_coding_prompt_filename}")
90
+ self._validate_prompt_file_exists(prompts_directory, refinement_coding_prompt_filename)
91
91
  self.refinement_coding_prompt_filename = refinement_coding_prompt_filename
92
92
 
93
93
 
@@ -98,25 +98,74 @@ class MCPConfig:
98
98
  self.logger.info(f" Refinement Coding Prompt Filename: {self.refinement_coding_prompt_filename}")
99
99
  self.logger.info(f" Default Provider: {self.default_provider}")
100
100
  for prov in self.providers:
101
- self.logger.info(f"Configured provider name: {prov["name"]} , api_endpoint: {prov["api_endpoint"]} , default_model: {prov["default_model"]}")
101
+ self.logger.info(f"Configured provider name: {prov['name']} , api_endpoint: {prov['api_endpoint']} , default_model: {prov['default_model']}")
102
+
103
+ def _validate_prompt_file_exists(self, prompts_directory: str, filename: str) -> None:
104
+ """Validate that a prompt file exists in the specified directory.
105
+
106
+ Args:
107
+ prompts_directory (str): Directory where prompt files are located
108
+ filename (str): Name of the prompt file to check
109
+
110
+ Raises:
111
+ FileNotFoundError: If the prompt file does not exist
112
+ """
113
+ if not os.path.exists(os.path.join(prompts_directory, filename)):
114
+ raise FileNotFoundError(f"Prompt file not found: {filename}")
115
+
116
+ def _validate_config_file_path(self, config_file_path: str) -> bool:
117
+ """Validate that the configuration file path is valid and accessible.
118
+
119
+ Args:
120
+ config_file_path (str): Path to the configuration file
121
+
122
+ Returns:
123
+ bool: True if path is valid and accessible, False otherwise
124
+ """
125
+ try:
126
+ # Check if we can write to the directory
127
+ dir_path = os.path.dirname(config_file_path) or "."
128
+ if not os.path.exists(dir_path):
129
+ os.makedirs(dir_path, exist_ok=True)
130
+ # Test that we can actually access the file path
131
+ Path(config_file_path).touch(exist_ok=True)
132
+ return True
133
+ except (OSError, IOError):
134
+ return False
102
135
 
103
- def available_providers(self):
104
- return list(map(lambda prov: {'name': prov['name'], 'api_endpoint': prov['api_endpoint'], 'type': prov['type']}, self.providers))
136
+ def available_providers(self) -> List[Dict[str, Any]]:
137
+ return [{'name': p['name'], 'api_endpoint': p['api_endpoint'], 'type': p['type']} for p in self.providers]
105
138
 
106
- def get_provider_by_name(self, provider_name):
107
- providers = list(filter(lambda x: x['name'] == provider_name, self.providers))
108
- return providers[0]
139
+ def get_provider_by_name(self, provider_name: str) -> Dict[str, Any]:
140
+ """Get a provider by its name.
141
+
142
+ Args:
143
+ provider_name (str): Name of the provider to find
144
+
145
+ Returns:
146
+ dict: Provider configuration dictionary
147
+
148
+ Raises:
149
+ IndexError: If no provider with the given name is found
150
+ """
151
+ for provider in self.providers:
152
+ if provider['name'] == provider_name:
153
+ return provider
154
+ raise IndexError(f"Provider '{provider_name}' not found")
109
155
 
110
- def get_default_provider(self):
156
+ def get_default_provider(self) -> Dict[str, Any]:
111
157
  return self.get_provider_by_name(self.default_provider)
112
158
 
113
- def _configure_providers(self, config_data):
159
+ def _configure_providers(self, config_data: Dict[str, Any]) -> None:
114
160
  # configure defaults if not config_data could be loaded
115
- self.providers = config_data.get("providers", {})
161
+ providers = config_data.get("providers", [])
162
+ if not isinstance(providers, list):
163
+ raise ValueError("'providers' must be a list in the configuration file")
164
+ self.providers = providers
116
165
  if len(self.providers) < 1:
117
- self.providers = [
118
- DEFAULT_PROVIDER_CONFIGURATION
119
- ]
166
+ # Validate defaults before use
167
+ self._validate_provider(DEFAULT_PROVIDER_CONFIGURATION)
168
+ self.providers = [DEFAULT_PROVIDER_CONFIGURATION]
120
169
  self.default_provider = DEFAULT_PROVIDER_NAME
121
170
  return
122
171
 
@@ -127,41 +176,42 @@ class MCPConfig:
127
176
  self._validate_provider(provider)
128
177
  provider_names.append(provider['name'])
129
178
 
130
- if not config_data['default_provider']:
179
+ if not config_data.get('default_provider'):
131
180
  raise ValueError(f"No default_provider was configured at the root level of the config file in {self.config_file_path}")
132
181
  self.default_provider = config_data['default_provider']
133
182
 
134
- def _validate_provider(self, provider):
183
+ def _validate_provider(self, provider: Dict[str, Any]) -> None:
135
184
  self._validate_provider_name(provider.get("name", ""))
136
185
  self._validate_provider_type(provider.get("type", ""))
137
186
  self._validate_url(provider.get("api_endpoint", ""))
138
187
  self._validate_api_key(provider.get("api_key", ""))
139
188
  self._validate_model_name(provider.get("default_model", ""))
140
189
 
141
- def _validate_provider_name(self, provider_name):
190
+ def _validate_provider_name(self, provider_name: str) -> None:
142
191
  if len(provider_name) < 1:
143
192
  raise ValueError(f"The provider name: {provider_name} is not a valid provider name")
144
193
 
145
- def _validate_provider_type(self, provider_type):
194
+ def _validate_provider_type(self, provider_type: str) -> None:
146
195
  if not provider_type in self.PROVIDER_TYPES:
147
196
  raise ValueError(f"The provider type: {provider_type} is not supported by sokrates-mcp")
148
197
 
149
- def _validate_url(self, url):
198
+ def _validate_url(self, url: str) -> None:
150
199
  """Validate URL format.
151
200
 
152
201
  Args:
153
202
  url (str): URL to validate
154
203
 
155
- Returns:
156
- bool: True if valid URL, False otherwise
204
+ Raises:
205
+ ValueError: If the URL is invalid
157
206
  """
158
207
  try:
159
208
  result = urlparse(url)
160
- return all([result.scheme in ['http', 'https'], result.netloc])
161
- except:
162
- raise ValueError(f"The api_endpoint: {url} is not a valid llm API endpoint")
209
+ if not (result.scheme in ['http', 'https'] and result.netloc):
210
+ raise ValueError(f"Invalid API endpoint: {url}")
211
+ except Exception as e:
212
+ raise ValueError(f"Invalid API endpoint format: {url}") from e
163
213
 
164
- def _validate_api_key(self, api_key):
214
+ def _validate_api_key(self, api_key: str) -> None:
165
215
  """Validate API key format.
166
216
 
167
217
  Args:
@@ -173,7 +223,7 @@ class MCPConfig:
173
223
  if len(api_key) < 1:
174
224
  raise ValueError("The api key is empty")
175
225
 
176
- def _validate_model_name(self, model):
226
+ def _validate_model_name(self, model: str) -> None:
177
227
  """Validate model name format.
178
228
 
179
229
  Args:
@@ -185,7 +235,7 @@ class MCPConfig:
185
235
  if len(model) < 1:
186
236
  raise ValueError("The model is empty")
187
237
 
188
- def _ensure_directory_exists(self, directory_path):
238
+ def _ensure_directory_exists(self, directory_path: str) -> bool:
189
239
  """Ensure directory exists and is valid.
190
240
 
191
241
  Args:
@@ -203,7 +253,7 @@ class MCPConfig:
203
253
  self.logger.error(f"Error ensuring directory exists: {e}")
204
254
  return False
205
255
 
206
- def _load_config_from_file(self, config_file_path):
256
+ def _load_config_from_file(self, config_file_path: str) -> Dict[str, Any]:
207
257
  """Load configuration data from a YAML file.
208
258
 
209
259
  Args:
@@ -224,13 +274,12 @@ class MCPConfig:
224
274
  with open(config_file_path, 'r') as f:
225
275
  return yaml.safe_load(f) or {}
226
276
  else:
227
- self.logger.warning(f"Config file not found at {config_file_path}. Using defaults.")
228
- # Create empty config file
229
- with open(config_file_path, 'w') as f:
230
- yaml.dump({}, f)
277
+ self.logger.warning(f"Config file not found at {config_file_path}. Using defaults (no config created).")
231
278
  return {}
232
279
  except yaml.YAMLError as e:
233
280
  self.logger.error(f"Error parsing YAML config file {config_file_path}: {e}")
281
+ except OSError as e:
282
+ self.logger.error(f"OS error reading config file {config_file_path}: {e}")
234
283
  except Exception as e:
235
- self.logger.error(f"Error reading config file {config_file_path}: {e}")
284
+ self.logger.error(f"Unexpected error reading config file {config_file_path}: {e}")
236
285
  return {}
sokrates_mcp/utils.py ADDED
@@ -0,0 +1,28 @@
1
+ import secrets
2
+
3
+ class Utils:
4
+
5
+ @staticmethod
6
+ def rand_int_inclusive(min_val: int, max_val: int) -> int:
7
+ """
8
+ Return a random integer N such that min_val <= N <= max_val.
9
+ Uses `secrets.randbelow` which is cryptographically secure.
10
+
11
+ Parameters
12
+ ----------
13
+ min_val : int
14
+ Lower bound (inclusive).
15
+ max_val : int
16
+ Upper bound (inclusive).
17
+
18
+ Returns
19
+ -------
20
+ int
21
+ Random integer in the specified range.
22
+ """
23
+ if min_val > max_val:
24
+ raise ValueError("min_val must be <= max_val")
25
+
26
+ # randbelow(n) returns 0 .. n-1. We need a window of size (max-min+1)
27
+ range_size = max_val - min_val + 1
28
+ return secrets.randbelow(range_size) + min_val
sokrates_mcp/workflow.py CHANGED
@@ -1,9 +1,14 @@
1
+ from pathlib import Path
2
+ from typing import List
3
+
1
4
  from fastmcp import Context
2
- from .mcp_config import MCPConfig
5
+
3
6
  from sokrates import FileHelper, RefinementWorkflow, LLMApi, PromptRefiner, IdeaGenerationWorkflow
4
7
  from sokrates.coding.code_review_workflow import run_code_review
5
- from pathlib import Path
6
- from typing import List
8
+
9
+ from .mcp_config import MCPConfig
10
+ from .utils import Utils
11
+
7
12
  class Workflow:
8
13
 
9
14
  WORKFLOW_COMPLETION_MESSAGE = "Workflow completed."
@@ -290,4 +295,49 @@ class Workflow:
290
295
  model_list = "\n".join([f"- {model}" for model in models])
291
296
  result = f"{api_headline}\n# List of available models\n{model_list}"
292
297
  await ctx.info(self.WORKFLOW_COMPLETION_MESSAGE)
298
+ return result
299
+
300
+ async def store_to_file(self, ctx: Context, file_path: str, file_content: str) -> str:
301
+ """Store the provided content to a file on disk
302
+
303
+ """
304
+ await ctx.info(f"Storing file to: {file_path} ...")
305
+ if not file_path:
306
+ raise ValueError("No file_path provided.")
307
+ if not file_content:
308
+ raise ValueError("No file_content provided.")
309
+
310
+ FileHelper.write_to_file(file_path=file_path, content=file_content)
311
+
312
+ result = f"The file has been stored to {file_path}"
313
+ await ctx.info(self.WORKFLOW_COMPLETION_MESSAGE)
314
+ return result
315
+
316
+ async def read_from_file(self, ctx: Context, file_path: str) -> str:
317
+ """Read content of the provided file path
318
+
319
+ """
320
+ await ctx.info(f"Reading file from: {file_path} ...")
321
+ if not file_path:
322
+ raise ValueError("No file_path provided.")
323
+ if not Path(file_path).is_file():
324
+ raise ValueError("No file exists at the given file path.")
325
+
326
+ content = FileHelper.read_file(file_path=file_path)
327
+ result = f"<file source_file_path='{file_path}'>\n{content}\n</file>"
328
+ await ctx.info(self.WORKFLOW_COMPLETION_MESSAGE)
329
+ return result
330
+
331
+ async def roll_dice(self, ctx: Context, number_of_dice: int=1, side_count: int=6, number_of_rolls: int=1) -> str:
332
+ """Roll a dice with the provided number of sides and return the result
333
+
334
+ """
335
+ await ctx.info(f"Throwing {number_of_dice} dice with {side_count} sides {number_of_rolls} times ...")
336
+ result = ""
337
+ for throw_number in range(1,number_of_rolls):
338
+ result = f"{result}# Roll {throw_number}\n"
339
+ for dice_number in range(1, number_of_dice):
340
+ dice_result = Utils.rand_int_inclusive(1, side_count)
341
+ result = f"- Dice {dice_number} result: {dice_result}\n"
342
+ await ctx.info(self.WORKFLOW_COMPLETION_MESSAGE)
293
343
  return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sokrates-mcp
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A templated MCP server for demonstration and quick start.
5
5
  Author-email: Julian Weber <julianweberdev@gmail.com>
6
6
  License: MIT License
@@ -224,7 +224,7 @@ The server follows a modular design pattern:
224
224
  ### workflow.py
225
225
 
226
226
  - **Workflow** class: Implements the business logic for prompt refinement and execution.
227
- - Methods:
227
+ - e.g.:
228
228
  - `refine_prompt`: Refines a given prompt
229
229
  - `refine_and_execute_external_prompt`: Refines and executes a prompt with an external LLM
230
230
  - `handover_prompt`: Hands over a prompt to an external LLM for processing
@@ -289,6 +289,13 @@ If you see "ModuleNotFoundError: fastmcp", ensure:
289
289
 
290
290
  ## Changelog
291
291
 
292
+ **0.3.0 (Aug 2025)**
293
+ - adds new tools:
294
+ - roll_dice
295
+ - read_from_file
296
+ - store_to_file
297
+ - refactorings - code quality - still ongoing
298
+
292
299
  **0.2.0 (Aug 2025)**
293
300
  - First published version
294
301
  - Update to latest sokrates library version
@@ -0,0 +1,13 @@
1
+ sokrates_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sokrates_mcp/main.py,sha256=yF-QgN5713Xapy8aOly_XOSZ9XSOWbSuHv5zwl_ynEI,22589
3
+ sokrates_mcp/mcp_config.py,sha256=5MnKhmZTCUhZQwhq1mlxuZwaAyC7Sw9VkMZ-DfMACoM,12395
4
+ sokrates_mcp/utils.py,sha256=LOXRXjuEVhqVjJeJ7Rnn7eAVwzZEbWz7Vn9pwVb9oac,795
5
+ sokrates_mcp/workflow.py,sha256=1vMGle8BE1bU8x1PjiQKaEcnZe4tdf2QN-lQjkAhe-c,15267
6
+ sokrates_mcp-0.3.0.dist-info/licenses/LICENSE,sha256=OgJ7nuNhaIefjDRK0wTGOErJ_c1984Eg9oUweycmal0,1068
7
+ sokrates_mcp_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ sokrates_mcp_client/mcp_client_example.py,sha256=L5_xH0u7lt0k0t_eiFFhN9FVU__seFhxHfRixdy14PU,3866
9
+ sokrates_mcp-0.3.0.dist-info/METADATA,sha256=fEbkt21NJ7EJOFW3ftossAD85-kag0VrSDFE00CdHi8,9616
10
+ sokrates_mcp-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ sokrates_mcp-0.3.0.dist-info/entry_points.txt,sha256=7gYOgoyRs_mE6dmwMJrAtrMns2mxv4ZbqXBznRh3sUc,56
12
+ sokrates_mcp-0.3.0.dist-info/top_level.txt,sha256=Nbwxz5Mm6LVkglOxqt4ZyEO5A6D4VjjN8c6d-fQyc3k,33
13
+ sokrates_mcp-0.3.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- sokrates_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sokrates_mcp/main.py,sha256=2zIm3lkP3xJyS-_w6oJb-qovdjjCw0D4oocAapgdzVA,20697
3
- sokrates_mcp/mcp_config.py,sha256=5LA72MwmoM8LpUNx4cUkU4e5Xif6nhL16I68JfRelAE,10089
4
- sokrates_mcp/workflow.py,sha256=OyiLFbh3bj8fBQYPt1YNjcj9HY3v--xu03PZVmGJgig,13439
5
- sokrates_mcp-0.2.0.dist-info/licenses/LICENSE,sha256=OgJ7nuNhaIefjDRK0wTGOErJ_c1984Eg9oUweycmal0,1068
6
- sokrates_mcp_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- sokrates_mcp_client/mcp_client_example.py,sha256=L5_xH0u7lt0k0t_eiFFhN9FVU__seFhxHfRixdy14PU,3866
8
- sokrates_mcp-0.2.0.dist-info/METADATA,sha256=vTdQwxkRk-1NaiVcIDIryvsLlBxWS6tuLGjlyIm-m8A,9475
9
- sokrates_mcp-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- sokrates_mcp-0.2.0.dist-info/entry_points.txt,sha256=7gYOgoyRs_mE6dmwMJrAtrMns2mxv4ZbqXBznRh3sUc,56
11
- sokrates_mcp-0.2.0.dist-info/top_level.txt,sha256=Nbwxz5Mm6LVkglOxqt4ZyEO5A6D4VjjN8c6d-fQyc3k,33
12
- sokrates_mcp-0.2.0.dist-info/RECORD,,