cnhkmcp 1.2.1__py3-none-any.whl → 1.2.3__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.
cnhkmcp/__init__.py CHANGED
@@ -50,7 +50,7 @@ from .untracked.forum_functions import (
50
50
  read_full_forum_post
51
51
  )
52
52
 
53
- __version__ = "1.2.1"
53
+ __version__ = "1.2.3"
54
54
  __author__ = "CNHK"
55
55
  __email__ = "cnhk@example.com"
56
56
 
@@ -26,6 +26,9 @@ from bs4 import BeautifulSoup
26
26
  from mcp.server.fastmcp import FastMCP
27
27
  from pydantic import BaseModel, Field, EmailStr
28
28
 
29
+ from pathlib import Path
30
+
31
+
29
32
  # Configure logging
30
33
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
31
34
  logger = logging.getLogger(__name__)
@@ -389,7 +392,7 @@ class BrainApiClient:
389
392
  response = self.session.get(f"{self.base_url}/data-sets", params=params)
390
393
  response.raise_for_status()
391
394
  response = response.json()
392
- response['extraNote'] = "if your returned result is 0, you may want to check your parameter by using get_instrument_options tool to got correct parameter"
395
+ response['extraNote'] = "if your returned result is 0, you may want to check your parameter by using get_platform_setting_options tool to got correct parameter"
393
396
  return response
394
397
  except Exception as e:
395
398
  self.log(f"Failed to get datasets: {str(e)}", "ERROR")
@@ -423,7 +426,7 @@ class BrainApiClient:
423
426
  response = self.session.get(f"{self.base_url}/data-fields", params=params)
424
427
  response.raise_for_status()
425
428
  response = response.json()
426
- response['extraNote'] = "if your returned result is 0, you may want to check your parameter by using get_instrument_options tool to got correct parameter"
429
+ response['extraNote'] = "if your returned result is 0, you may want to check your parameter by using get_platform_setting_options tool to got correct parameter"
427
430
  return response
428
431
  except Exception as e:
429
432
  self.log(f"Failed to get datafields: {str(e)}", "ERROR")
@@ -1062,7 +1065,7 @@ class BrainApiClient:
1062
1065
  self.log(f"Failed to get competition agreement: {str(e)}", "ERROR")
1063
1066
  raise
1064
1067
 
1065
- async def get_instrument_options(self) -> Dict[str, Any]:
1068
+ async def get_platform_setting_options(self) -> Dict[str, Any]:
1066
1069
  """Get available instrument types, regions, delays, and universes."""
1067
1070
  await self.ensure_authenticated()
1068
1071
 
@@ -1212,42 +1215,80 @@ brain_client = BrainApiClient()
1212
1215
  # Configuration management
1213
1216
  CONFIG_FILE = "user_config.json"
1214
1217
 
1218
+ def _resolve_config_path(for_write: bool = False) -> str:
1219
+ """
1220
+ Resolve the config file path with this priority:
1221
+ 1) BRAIN_CONFIG_PATH (file or directory)
1222
+ 2) Directory of running script when available, else current working directory
1223
+ 3) Current working directory
1224
+
1225
+ When for_write=True, returns the preferred path even if it doesn't exist yet.
1226
+ """
1227
+ # 1) Explicit override via env var
1228
+ env_path = os.environ.get("BRAIN_CONFIG_PATH")
1229
+ if env_path:
1230
+ p = Path(env_path).expanduser()
1231
+ target = p / CONFIG_FILE if p.is_dir() else p
1232
+ # For read, only if it exists; for write, allow regardless
1233
+ if for_write or target.exists():
1234
+ return str(target.resolve())
1235
+
1236
+ # 2) Script/module directory when available, else CWD (works in notebooks)
1237
+ base_dir = Path.cwd()
1238
+ try:
1239
+ # __file__ is not defined in notebooks; this will fail there and keep CWD
1240
+ script_dir = Path(__file__).resolve().parent # type: ignore[name-defined]
1241
+ base_dir = script_dir
1242
+ except Exception:
1243
+ # Fall back to current working directory for notebooks/REPL
1244
+ pass
1245
+
1246
+ module_path = base_dir / CONFIG_FILE
1247
+ if not for_write and module_path.exists():
1248
+ return str(module_path.resolve())
1249
+
1250
+ # 3) Fallback to CWD for backward compatibility
1251
+ cwd_path = Path.cwd() / CONFIG_FILE
1252
+ if not for_write and cwd_path.exists():
1253
+ return str(cwd_path.resolve())
1254
+
1255
+ # For writes (or when nothing exists), prefer the module/base directory
1256
+ return str(module_path.resolve())
1257
+
1215
1258
  def load_config() -> Dict[str, Any]:
1216
- """Load configuration from file."""
1217
- if os.path.exists(CONFIG_FILE):
1259
+ """Load configuration from file with robust path resolution.
1260
+
1261
+ Looks for the config in this order: BRAIN_CONFIG_PATH -> module directory -> CWD.
1262
+ Returns an empty dict when not found or on error.
1263
+ """
1264
+ path = _resolve_config_path(for_write=False)
1265
+ if os.path.exists(path):
1218
1266
  try:
1219
- with open(CONFIG_FILE, 'r') as f:
1267
+ with open(path, 'r', encoding='utf-8') as f:
1220
1268
  return json.load(f)
1221
1269
  except Exception as e:
1222
- logger.error(f"Failed to load config: {e}")
1270
+ logger.error(f"Failed to load config from '{path}': {e}")
1223
1271
  return {}
1224
1272
 
1225
- def load_brain_credentials() -> Optional[tuple]:
1226
- """Load credentials from .brain_credentials file in home directory."""
1227
- try:
1228
- from os.path import expanduser
1229
- credentials_file = expanduser('~/.brain_credentials')
1230
- if os.path.exists(credentials_file):
1231
- with open(credentials_file, 'r') as f:
1232
- credentials = json.load(f)
1233
- if isinstance(credentials, list) and len(credentials) == 2:
1234
- return tuple(credentials)
1235
- except Exception as e:
1236
- logger.error(f"Failed to load .brain_credentials: {e}")
1237
- return None
1238
1273
 
1239
1274
  def save_config(config: Dict[str, Any]):
1240
- """Save configuration to file."""
1275
+ """Save configuration to file using the resolved config path.
1276
+
1277
+ Uses BRAIN_CONFIG_PATH if set; otherwise writes next to this module.
1278
+ Ensures the target directory exists.
1279
+ """
1241
1280
  try:
1242
- with open(CONFIG_FILE, 'w') as f:
1243
- json.dump(config, f, indent=2)
1281
+ path = _resolve_config_path(for_write=True)
1282
+ os.makedirs(os.path.dirname(path), exist_ok=True)
1283
+ with open(path, 'w', encoding='utf-8') as f:
1284
+ json.dump(config, f, indent=2, ensure_ascii=False)
1244
1285
  except Exception as e:
1245
1286
  logger.error(f"Failed to save config: {e}")
1246
1287
 
1247
1288
  # MCP Tools
1248
1289
 
1249
1290
  @mcp.tool()
1250
- async def authenticate(email: str = "", password: str = "") -> Dict[str, Any]:
1291
+ async def authenticate(email: Optional[str] = "", password: Optional[str] = "") -> Dict[str, Any]:
1251
1292
  """
1252
1293
  🔐 Authenticate with WorldQuant BRAIN platform.
1253
1294
 
@@ -1261,20 +1302,12 @@ async def authenticate(email: str = "", password: str = "") -> Dict[str, Any]:
1261
1302
  Authentication result with user info and permissions
1262
1303
  """
1263
1304
  try:
1264
- # First try to load from .brain_credentials file
1265
- brain_creds = load_brain_credentials()
1266
- if brain_creds and not email and not password:
1267
- email, password = brain_creds
1268
- print(f"📧 Using credentials from .brain_credentials file: {email}")
1269
-
1270
- # If still no credentials, try config file
1271
- if not email or not password:
1272
- config = load_config()
1273
- if 'credentials' in config:
1274
- if not email:
1275
- email = config['credentials'].get('email', '')
1276
- if not password:
1277
- password = config['credentials'].get('password', '')
1305
+ config = load_config()
1306
+ if 'credentials' in config:
1307
+ if not email:
1308
+ email = config['credentials'].get('email', '')
1309
+ if not password:
1310
+ password = config['credentials'].get('password', '')
1278
1311
 
1279
1312
  if not email or not password:
1280
1313
  return {"error": "Email and password required. Either provide them as arguments, configure them in user_config.json, or create a .brain_credentials file in your home directory with format: [\"email\", \"password\"]"}
@@ -1946,10 +1979,18 @@ async def get_competition_agreement(competition_id: str) -> Dict[str, Any]:
1946
1979
  return {"error": str(e)}
1947
1980
 
1948
1981
  @mcp.tool()
1949
- async def get_instrument_options() -> Dict[str, Any]:
1950
- """Get available instrument types, regions, delays, and universes."""
1982
+ async def get_platform_setting_options() -> Dict[str, Any]:
1983
+ """Discover valid simulation setting options (instrument types, regions, delays, universes, neutralization).
1984
+
1985
+ Use this when a simulation request might contain an invalid/mismatched setting. If an AI or user supplies
1986
+ incorrect parameters (e.g., wrong region for an instrument type), call this tool to retrieve the authoritative
1987
+ option sets and correct the inputs before proceeding.
1988
+
1989
+ Returns:
1990
+ A structured list of valid combinations and choice lists to validate or fix simulation settings.
1991
+ """
1951
1992
  try:
1952
- return await brain_client.get_instrument_options()
1993
+ return await brain_client.get_platform_setting_options()
1953
1994
  except Exception as e:
1954
1995
  return {"error": str(e)}
1955
1996
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cnhkmcp
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: A comprehensive Model Context Protocol (MCP) server for quantitative trading platform integration
5
5
  Home-page: https://github.com/cnhk/cnhkmcp
6
6
  Author: CNHK
@@ -0,0 +1,10 @@
1
+ cnhkmcp/__init__.py,sha256=ggwv1_2vTzA1CqKNjy2TavuVVVZ5ByMzSN8Vvt4Hu1g,2758
2
+ cnhkmcp/untracked/forum_functions.py,sha256=78wzvN_UYWwbWU40q8_FJNSFPJnND6W9ZRey6MSSiEk,45516
3
+ cnhkmcp/untracked/platform_functions.py,sha256=fmNz6DVfcpyz8cEWk7JbW3R5-3kbez4zOe520c6FMwQ,81674
4
+ cnhkmcp/untracked/user_config.json,sha256=_INn1X1qIsITrmEno-BRlQOAGm9wnNCw-6B333DEvnk,695
5
+ cnhkmcp-1.2.3.dist-info/licenses/LICENSE,sha256=QLxO2eNMnJQEdI_R1UV2AOD-IvuA8zVrkHWA4D9gtoc,1081
6
+ cnhkmcp-1.2.3.dist-info/METADATA,sha256=Q-o8ejvy53gOpLHwfz1lv5xb0EkGuNoEwMgeqn-ahnE,5171
7
+ cnhkmcp-1.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ cnhkmcp-1.2.3.dist-info/entry_points.txt,sha256=lTQieVyIvjhSMK4fT-XwnccY-JBC1H4vVQ3V9dDM-Pc,70
9
+ cnhkmcp-1.2.3.dist-info/top_level.txt,sha256=x--ibUcSgOS9Z_RWK2Qc-vfs7DaXQN-WMaaxEETJ1Bw,8
10
+ cnhkmcp-1.2.3.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- cnhkmcp/__init__.py,sha256=P9ZwPprkFWPMsX6zdprKviA-9LODjQ-eocdzhLiZNkg,2758
2
- cnhkmcp/untracked/forum_functions.py,sha256=78wzvN_UYWwbWU40q8_FJNSFPJnND6W9ZRey6MSSiEk,45516
3
- cnhkmcp/untracked/platform_functions.py,sha256=LQ0s1dASC_2KoZ5XhRf4G2mRZERMN2bScOklnRVyfdQ,80066
4
- cnhkmcp/untracked/user_config.json,sha256=_INn1X1qIsITrmEno-BRlQOAGm9wnNCw-6B333DEvnk,695
5
- cnhkmcp-1.2.1.dist-info/licenses/LICENSE,sha256=QLxO2eNMnJQEdI_R1UV2AOD-IvuA8zVrkHWA4D9gtoc,1081
6
- cnhkmcp-1.2.1.dist-info/METADATA,sha256=D1f88diDrbkAj8XrL0aGTodOl74L5KzIY-5XvEPfbkE,5171
7
- cnhkmcp-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- cnhkmcp-1.2.1.dist-info/entry_points.txt,sha256=lTQieVyIvjhSMK4fT-XwnccY-JBC1H4vVQ3V9dDM-Pc,70
9
- cnhkmcp-1.2.1.dist-info/top_level.txt,sha256=x--ibUcSgOS9Z_RWK2Qc-vfs7DaXQN-WMaaxEETJ1Bw,8
10
- cnhkmcp-1.2.1.dist-info/RECORD,,