cmdbox 0.6.1.1__py3-none-any.whl → 0.6.2__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.

Potentially problematic release.


This version of cmdbox might be problematic. Click here for more details.

Files changed (71) hide show
  1. cmdbox/app/auth/signin.py +7 -3
  2. cmdbox/app/edge.py +3 -3
  3. cmdbox/app/features/cli/agent_base.py +42 -42
  4. cmdbox/app/features/cli/audit_base.py +28 -28
  5. cmdbox/app/features/cli/cmdbox_audit_createdb.py +28 -28
  6. cmdbox/app/features/cli/cmdbox_audit_delete.py +26 -26
  7. cmdbox/app/features/cli/cmdbox_audit_search.py +42 -42
  8. cmdbox/app/features/cli/cmdbox_audit_write.py +22 -22
  9. cmdbox/app/features/cli/cmdbox_client_file_copy.py +36 -36
  10. cmdbox/app/features/cli/cmdbox_client_file_download.py +38 -38
  11. cmdbox/app/features/cli/cmdbox_client_file_list.py +34 -34
  12. cmdbox/app/features/cli/cmdbox_client_file_mkdir.py +32 -32
  13. cmdbox/app/features/cli/cmdbox_client_file_move.py +34 -34
  14. cmdbox/app/features/cli/cmdbox_client_file_remove.py +32 -32
  15. cmdbox/app/features/cli/cmdbox_client_file_rmdir.py +32 -32
  16. cmdbox/app/features/cli/cmdbox_client_file_upload.py +38 -38
  17. cmdbox/app/features/cli/cmdbox_client_server_info.py +26 -26
  18. cmdbox/app/features/cli/cmdbox_cmd_list.py +22 -21
  19. cmdbox/app/features/cli/cmdbox_cmd_load.py +24 -20
  20. cmdbox/app/features/cli/cmdbox_edge_config.py +40 -40
  21. cmdbox/app/features/cli/cmdbox_edge_start.py +4 -4
  22. cmdbox/app/features/cli/cmdbox_gui_start.py +2 -2
  23. cmdbox/app/features/cli/cmdbox_gui_stop.py +2 -2
  24. cmdbox/app/features/cli/cmdbox_mcp_proxy.py +17 -11
  25. cmdbox/app/features/cli/cmdbox_server_list.py +20 -20
  26. cmdbox/app/features/cli/cmdbox_server_start.py +26 -26
  27. cmdbox/app/features/cli/cmdbox_server_stop.py +26 -26
  28. cmdbox/app/features/cli/cmdbox_web_apikey_add.py +24 -24
  29. cmdbox/app/features/cli/cmdbox_web_apikey_del.py +24 -24
  30. cmdbox/app/features/cli/cmdbox_web_gencert.py +24 -24
  31. cmdbox/app/features/cli/cmdbox_web_genpass.py +20 -20
  32. cmdbox/app/features/cli/cmdbox_web_group_add.py +26 -26
  33. cmdbox/app/features/cli/cmdbox_web_group_del.py +22 -22
  34. cmdbox/app/features/cli/cmdbox_web_group_edit.py +26 -26
  35. cmdbox/app/features/cli/cmdbox_web_group_list.py +22 -22
  36. cmdbox/app/features/cli/cmdbox_web_start.py +65 -65
  37. cmdbox/app/features/cli/cmdbox_web_stop.py +10 -10
  38. cmdbox/app/features/cli/cmdbox_web_user_add.py +32 -32
  39. cmdbox/app/features/cli/cmdbox_web_user_del.py +22 -22
  40. cmdbox/app/features/cli/cmdbox_web_user_edit.py +32 -32
  41. cmdbox/app/features/cli/cmdbox_web_user_list.py +22 -22
  42. cmdbox/app/features/web/cmdbox_web_agent.py +0 -4
  43. cmdbox/app/mcp.py +316 -120
  44. cmdbox/app/options.py +21 -21
  45. cmdbox/app/web.py +1 -1
  46. cmdbox/extensions/sample_project/sample/app/features/cli/sample_client_time.py +4 -4
  47. cmdbox/extensions/sample_project/sample/app/features/cli/sample_server_time.py +18 -18
  48. cmdbox/extensions/sample_project/sample/extensions/user_list.yml +4 -0
  49. cmdbox/extensions/user_list.yml +4 -0
  50. cmdbox/licenses/LICENSE_dnspython_2_7_0_ISC_License-ISCL.txt +35 -0
  51. cmdbox/licenses/LICENSE_email_validator_2_2_0_The_Unlicense-Unlicense.txt +27 -0
  52. cmdbox/licenses/files.txt +4 -2
  53. cmdbox/version.py +2 -2
  54. cmdbox/web/agent.html +2 -2
  55. cmdbox/web/assets/cmdbox/audit.js +14 -14
  56. cmdbox/web/assets/cmdbox/common.js +21 -7
  57. cmdbox/web/assets/cmdbox/list_cmd.js +5 -5
  58. cmdbox/web/assets/cmdbox/signin.js +17 -7
  59. cmdbox/web/assets/cmdbox/svgicon.js +3 -3
  60. cmdbox/web/assets/cmdbox/users.js +14 -5
  61. cmdbox/web/audit.html +6 -6
  62. cmdbox/web/signin.html +13 -10
  63. cmdbox/web/users.html +4 -4
  64. {cmdbox-0.6.1.1.dist-info → cmdbox-0.6.2.dist-info}/METADATA +27 -23
  65. {cmdbox-0.6.1.1.dist-info → cmdbox-0.6.2.dist-info}/RECORD +71 -69
  66. /cmdbox/licenses/{LICENSE_fastmcp_2_9_2_Apache_Software_License.txt → LICENSE_fastmcp_2_10_1_Apache_Software_License.txt} +0 -0
  67. /cmdbox/licenses/{LICENSE_mcp_1_9_4_MIT_License.txt → LICENSE_mcp_1_10_1_MIT_License.txt} +0 -0
  68. {cmdbox-0.6.1.1.dist-info → cmdbox-0.6.2.dist-info}/LICENSE +0 -0
  69. {cmdbox-0.6.1.1.dist-info → cmdbox-0.6.2.dist-info}/WHEEL +0 -0
  70. {cmdbox-0.6.1.1.dist-info → cmdbox-0.6.2.dist-info}/entry_points.txt +0 -0
  71. {cmdbox-0.6.1.1.dist-info → cmdbox-0.6.2.dist-info}/top_level.txt +0 -0
cmdbox/app/mcp.py CHANGED
@@ -1,4 +1,4 @@
1
- from cmdbox.app import common, options
1
+ from cmdbox.app import common, feature
2
2
  from cmdbox.app.options import Options
3
3
  from cmdbox.app.auth import signin
4
4
  from pathlib import Path
@@ -35,12 +35,14 @@ class Mcp:
35
35
  self.ver = ver
36
36
  self.signin = sign
37
37
 
38
- def create_mcpserver(self, args:argparse.Namespace) -> Any:
38
+ def create_mcpserver(self, args:argparse.Namespace, tools:List[Any], web:Any) -> Any:
39
39
  """
40
40
  mcpserverを作成します
41
41
 
42
42
  Args:
43
43
  args (argparse.Namespace): 引数
44
+ tools (List[Any]): ツールのリスト
45
+ web (Any): Web関連のオブジェクト
44
46
 
45
47
  Returns:
46
48
  Any: FastMCP
@@ -58,10 +60,13 @@ class Mcp:
58
60
  issuer=issuer,
59
61
  audience=audience
60
62
  )
61
- mcp = FastMCP(name=self.ver.__appid__, auth=auth)
63
+ mcp = FastMCP(name=self.ver.__appid__, tools=tools, auth=auth)
62
64
  else:
63
65
  self.logger.info(f"Using BearerAuthProvider without public key, issuer, or audience.")
64
- mcp = FastMCP(name=self.ver.__appid__)
66
+ mcp = FastMCP(name=self.ver.__appid__, tools=tools)
67
+ mcp.add_middleware(self.create_mw_logging(self.logger, args))
68
+ mcp.add_middleware(self.create_mw_reqscope(self.logger, args, web))
69
+ mcp.add_middleware(self.create_mw_toollist(self.logger, args))
65
70
  return mcp
66
71
 
67
72
  def create_session_service(self, args:argparse.Namespace) -> Any:
@@ -119,7 +124,8 @@ class Mcp:
119
124
  f"ユーザーがコマンドを実行したいとき、あなたは以下の手順に従ってコマンドを確実に実行してください。\n" + \
120
125
  f"1. ユーザーのクエリからが実行したいコマンドを特定します。\n" + \
121
126
  f"2. コマンド実行に必要なパラメータのなかで、ユーザーのクエリから取得できないものは、コマンド定義にあるデフォルト値を指定して実行してください。\n" + \
122
- f"3. もしエラーが発生した場合は、ユーザーにコマンド名とパラメータとエラー内容を提示してください。\n"
127
+ f"3. もしエラーが発生した場合は、ユーザーにコマンド名とパラメータとエラー内容を提示してください。\n" \
128
+ f"4. コマンドの実行結果は、json文字列で出力するようにしてください。この時json文字列は「```json」と「```」で囲んだ文字列にしてください。\n"
123
129
 
124
130
  description = description if is_japan else \
125
131
  f"Command offer registered in {self.ver.__appid__}."
@@ -128,7 +134,8 @@ class Mcp:
128
134
  f"When a user wants to execute a command, you follow these steps to ensure that the command is executed.\n" + \
129
135
  f"1. Identify the command you want to execute from the user's query.\n" + \
130
136
  f"2. Any parameters required to execute the command that cannot be obtained from the user's query should be executed with the default values provided in the command definition.\n" + \
131
- f"3. If an error occurs, provide the user with the command name, parameters, and error description.\n"
137
+ f"3. If an error occurs, provide the user with the command name, parameters, and error description.\n" \
138
+ f"4. The result of the command execution should be output as a json string. The json string should be a string enclosed in '```json' and '```'."
132
139
 
133
140
  description = args.agent_description if args.agent_description else description
134
141
  instruction = args.agent_instruction if args.agent_instruction else instruction
@@ -235,13 +242,311 @@ class Mcp:
235
242
  session_service=session_service,
236
243
  )
237
244
 
238
- def init_agent_runner(self, logger:logging.Logger, args:argparse.Namespace) -> Tuple[Any, Any]:
245
+ def _to_schema(self, o:Dict[str, Any], is_japan:bool) -> Dict[str, Any]:
246
+ t, m = o["type"], o["multi"]
247
+ title = o['opt'].title().replace('_', ' ')
248
+ description = o['description_ja'] if is_japan else o['description_en']
249
+ if t == Options.T_BOOL:
250
+ return dict(title=title, type="array", items=dict(type="boolean"), description=description) if m \
251
+ else dict(title=title, type="boolean", description=description)
252
+ if t == Options.T_DATE:
253
+ return dict(title=title, type="array", items=dict(type="string"), description=description) if m \
254
+ else dict(title=title, type="string", description=description)
255
+ if t == Options.T_DATETIME:
256
+ return dict(title=title, type="array", items=dict(type="string"), description=description) if m \
257
+ else dict(title=title, type="string", description=description)
258
+ if t == Options.T_DICT:
259
+ return dict(title=title, type="array", items=dict(additionalProperties=True, type="object"), description=description) if m \
260
+ else dict(title=title, type="object", description=description)
261
+ if t == Options.T_DIR or t == Options.T_FILE:
262
+ return dict(title=title, type="array", items=dict(type="string"), description=description) if m \
263
+ else dict(title=title, type="string", description=description)
264
+ if t == Options.T_FLOAT:
265
+ return dict(title=title, type="array", items=dict(type="number"), description=description) if m \
266
+ else dict(title=title, type="number", description=description)
267
+ if t == Options.T_INT:
268
+ return dict(title=title, type="array", items=dict(type="integer"), description=description) if m \
269
+ else dict(title=title, type="integer", description=description)
270
+ if t == Options.T_STR or t == Options.T_TEXT:
271
+ return dict(title=title, type="array", items=dict(type="string"), description=description) if m \
272
+ else dict(title=title, type="string", description=description)
273
+ raise ValueError(f"Unknown type: {t} for option {o['opt']}")
274
+
275
+ def _ds(self, d:str) -> str:
276
+ return f'"{d}"' if d is not None else 'None'
277
+
278
+ def _doc_arg_type(self, o:Dict[str, Any], use_default:True) -> str:
279
+ t, m, d, r = o["type"], o["multi"], o["default"], o["required"]
280
+ ret = ""
281
+ dft = "None"
282
+ if t == Options.T_BOOL:
283
+ ret = "List[bool]" if m else f"bool"
284
+ dft = "[]" if m else f"{d}"
285
+ elif t == Options.T_DATE:
286
+ ret = "List[str]" if m else f"str"
287
+ dft = "[]" if m else self._ds(d)
288
+ elif t == Options.T_DATETIME:
289
+ ret = "List[str]" if m else f"str"
290
+ dft = "[]" if m else self._ds(d)
291
+ elif t == Options.T_DICT:
292
+ ret = "Dict" if m else f"Dict"
293
+ dft = "{}" if m else self._ds(d)
294
+ elif t == Options.T_DIR or t == Options.T_FILE:
295
+ if d is not None: d = str(d).replace('\\', '/')
296
+ ret = "List[str]" if m else f"str"
297
+ dft = "[]" if m else self._ds(d)
298
+ elif t == Options.T_FLOAT:
299
+ ret ="List[float]" if m else f"float"
300
+ dft ="[]" if m else f"{d}"
301
+ elif t == Options.T_INT:
302
+ ret = "List[int]" if m else f"int"
303
+ dft = "[]" if m else f"{d}"
304
+ elif t == Options.T_STR or t == Options.T_TEXT:
305
+ ret = "List[str]" if m else f"str"
306
+ dft = "[]" if m else self._ds(d)
307
+ else:
308
+ raise ValueError(f"Unknown type: {t} for option {o['opt']}")
309
+ return f"{ret}={dft}" if use_default else ret
310
+
311
+ def _doc_arg(self, o:Dict[str, Any], is_japan) -> str:
312
+ s = f' {o["opt"]}:{self._doc_arg_type(o, True)} '
313
+ s += f'{o["description_ja"] if is_japan else o["description_en"]}'
314
+ return s
315
+
316
+ def _create_func_txt(self, func_name:str, mode:str, cmd:str, is_japan:bool, options:Options, title:str='') -> str:
317
+ description = options.get_cmd_attr(mode, cmd, 'description_ja' if is_japan else 'description_en')
318
+ choices = options.get_cmd_choices(mode, cmd, False)
319
+ func_doc_args = "\n".join([self._doc_arg(o, is_japan) for o in choices])
320
+ func_txt = f"def {func_name}(*args, **kwargs):\n"
321
+ func_txt += f' """\n'
322
+ func_txt += f' {func_name} - MCP Tool Function\n'
323
+ func_txt += f' {description}\n'
324
+ func_txt += f'\n'
325
+ func_txt += f' Args:\n'
326
+ func_txt += f' {func_doc_args}\n'
327
+ func_txt += f'\n'
328
+ func_txt += f' Returns:\n'
329
+ func_txt += f' Dict[str, Any]: 実行結果\n'
330
+ func_txt += f' """\n'
331
+ func_txt += f' logger = logging.getLogger("web")\n'
332
+ func_txt += ' opt = {o["opt"]: kwargs.get(o["opt"], o["default"]) for o in options.get_cmd_choices("'+mode+'", "'+cmd+'", False)}\n'
333
+ func_txt += f' opt["data"] = Path(opt["data"]) if hasattr(opt, "data") else common.HOME_DIR / f".{self.ver.__appid__}"\n'
334
+ func_txt += f' if "{title}":\n'
335
+ func_txt += f' opt_path = opt["data"] / ".cmds" / f"cmd-{title}.json"\n'
336
+ func_txt += f' opt.update(common.loadopt(opt_path))\n'
337
+ func_txt += f' scope = signin.get_request_scope()\n'
338
+ func_txt += f' opt["mode"] = "{mode}"\n'
339
+ func_txt += f' opt["cmd"] = "{cmd}"\n'
340
+ func_txt += f' opt["format"] = False\n'
341
+ func_txt += f' opt["output_json"] = None\n'
342
+ func_txt += f' opt["output_json_append"] = False\n'
343
+ func_txt += f' opt["debug"] = logger.level == logging.DEBUG\n'
344
+ func_txt += f' opt["signin_file"] = scope["web"].signin_file\n'
345
+ func_txt += f' args = argparse.Namespace(**opt)\n'
346
+ func_txt += f' signin_data = signin.Signin.load_signin_file(args.signin_file)\n'
347
+ func_txt += f' req = scope["req"] if scope["req"] is not None else scope["websocket"]\n'
348
+ func_txt += f' sign = signin.Signin._check_signin(req, scope["res"], signin_data, logger)\n'
349
+ func_txt += f' if sign is not None:\n'
350
+ func_txt += f' logger.warning("Unable to execute command because authentication information cannot be obtained")\n'
351
+ func_txt += f' return dict(warn="Unable to execute command because authentication information cannot be obtained")\n'
352
+ func_txt += f' groups = req.session["signin"]["groups"]\n'
353
+ func_txt += f' if not signin.Signin._check_cmd(signin_data, groups, "{mode}", "{cmd}", logger):\n'
354
+ func_txt += f' logger.warning("You do not have permission to execute this command.")\n'
355
+ func_txt += f' return dict(warn="You do not have permission to execute this command.")\n'
356
+ func_txt += f' feat = options.get_cmd_attr("{mode}", "{cmd}", "feature")\n'
357
+ func_txt += f' args.groups = groups\n'
358
+ func_txt += f' try:\n'
359
+ func_txt += f' st, ret, _ = feat.apprun(logger, args, time.perf_counter(), [])\n'
360
+ func_txt += f' return ret\n'
361
+ func_txt += f' except Exception as e:\n'
362
+ func_txt += f' logger.error("Error occurs when tool is executed:", exc_info=True)\n'
363
+ func_txt += f' raise e\n'
364
+ func_txt += f'func_ctx.append({func_name})\n'
365
+ return func_txt
366
+
367
+ def create_tools(self, logger:logging.Logger, args:argparse.Namespace) -> List[Any]:
368
+ """
369
+ ツールリストを作成します
370
+
371
+ Args:
372
+ logger (logging.Logger): ロガー
373
+ args (argparse.Namespace): 引数
374
+
375
+ Returns:
376
+ List[Any]: fastmcp.tools.FunctionToolのリスト
377
+ """
378
+ from fastmcp.tools import FunctionTool
379
+ options = Options.getInstance()
380
+ language, _ = locale.getlocale()
381
+ is_japan = language.find('Japan') >= 0 or language.find('ja_JP') >= 0
382
+ func_tools:List[FunctionTool] = []
383
+ for mode in options.get_mode_keys():
384
+ for cmd in options.get_cmd_keys(mode):
385
+ if not options.get_cmd_attr(mode, cmd, 'use_agent'):
386
+ continue
387
+ # コマンドの説明と選択肢を取得
388
+ description = options.get_cmd_attr(mode, cmd, 'description_ja' if is_japan else 'description_en')
389
+ choices = options.get_cmd_choices(mode, cmd, False)
390
+ func_name = f"{mode}_{cmd}"
391
+ # 関数の定義を生成
392
+ func_txt = self._create_func_txt(func_name, mode, cmd, is_japan, options)
393
+ if logger.level == logging.DEBUG:
394
+ logger.debug(f"generating agent tool: {func_name}")
395
+ func_ctx = []
396
+ # 関数を実行してコンテキストに追加
397
+ exec(func_txt,
398
+ dict(time=time,List=List, Path=Path, argparse=argparse, common=common, options=options, logging=logging, signin=signin,),
399
+ dict(func_ctx=func_ctx))
400
+ # 関数のスキーマを生成
401
+ input_schema = dict(
402
+ type="object",
403
+ properties={o['opt']: self._to_schema(o, is_japan) for o in choices},
404
+ required=[o['opt'] for o in choices if o['required']],
405
+ )
406
+ output_schema = dict(type="object", properties=dict())
407
+ func_tool = FunctionTool(fn=func_ctx[0], name=func_name, title=func_name.title(), description=description,
408
+ tags=[f"mode={mode}", f"cmd={cmd}"],
409
+ parameters=input_schema, output_schema=output_schema,)
410
+ # ツールリストに追加
411
+ func_tools.append(func_tool)
412
+ return func_tools
413
+
414
+ def create_mw_toollist(self, logger:logging.Logger, args:argparse.Namespace) -> Any:
415
+ """
416
+ ツールリストを作成するミドルウェアを作成します
417
+
418
+ Args:
419
+ logger (logging.Logger): ロガー
420
+ args (argparse.Namespace): 引数
421
+
422
+ Returns:
423
+ Any: ミドルウェア
424
+ """
425
+ from cmdbox.app.web import Web
426
+ from fastmcp.server.middleware import Middleware, MiddlewareContext, ListToolsResult
427
+ from fastmcp.tools import FunctionTool
428
+ func_tools:List[FunctionTool] = self.create_tools(logger, args)
429
+ options = Options.getInstance()
430
+ cmd_list:feature.Feature = options.get_cmd_attr('cmd', 'list', "feature")
431
+ language, _ = locale.getlocale()
432
+ is_japan = language.find('Japan') >= 0 or language.find('ja_JP') >= 0
433
+ mcp = self
434
+ class CommandListMiddleware(Middleware):
435
+ async def on_list_tools(self, context: MiddlewareContext, call_next):
436
+ # 認証情報の取得
437
+ scope = signin.get_request_scope()
438
+ web:Web = scope["web"]
439
+ signin_file = web.signin_file
440
+ signin_data = signin.Signin.load_signin_file(signin_file)
441
+ if signin.Signin._check_signin(scope["req"], scope["res"], signin_data, logger) is not None:
442
+ logger.warning("Unable to execute command because authentication information cannot be obtained")
443
+ return dict(warn="Unable to execute command because authentication information cannot be obtained")
444
+ groups = scope["req"].session["signin"]["groups"]
445
+ ret_tools = []
446
+ # システムコマンドリストのフィルタリング
447
+ for func in func_tools:
448
+ mode = [t.replace('mode=', '') for t in func.tags if t.startswith('mode=')]
449
+ mode = mode[0] if mode else None
450
+ cmd = [t.replace('cmd=', '') for t in func.tags if t.startswith('cmd=')]
451
+ cmd = cmd[0] if cmd else None
452
+ if mode is None or cmd is None:
453
+ logger.warning(f"Tool {func.name} does not have mode or cmd tag, skipping.")
454
+ continue
455
+ if not signin.Signin._check_cmd(signin_data, groups, mode, cmd, logger):
456
+ logger.warning(f"User does not have permission to use tool {func.name} (mode={mode}, cmd={cmd}), skipping.")
457
+ continue
458
+ ret_tools.append(func)
459
+ # ユーザーコマンドリストの取得
460
+ args = argparse.Namespace(data=web.data, signin_file=signin_file, groups=groups, kwd=None,
461
+ format=False, output_json=None, output_json_append=False,)
462
+ st, ret, _ = cmd_list.apprun(logger, args, time.perf_counter(), [])
463
+ if ret is None or 'success' not in ret or not ret['success']:
464
+ return ret_tools
465
+ for opt in ret['success']:
466
+ func_name = f"user_{opt['title']}"
467
+ mode, cmd, description = opt['mode'], opt['cmd'], opt['description'] if 'description' in opt and opt['description'] else ''
468
+ choices = options.get_cmd_choices(mode, cmd, False)
469
+ description += '\n' + options.get_cmd_attr(mode, cmd, 'description_ja' if is_japan else 'description_en')
470
+ # 関数の定義を生成
471
+ func_txt = mcp._create_func_txt(func_name, mode, cmd, is_japan, options, title=opt['title'])
472
+ if logger.level == logging.DEBUG:
473
+ logger.debug(f"generating agent tool: {func_name}")
474
+ func_ctx = []
475
+ # 関数を実行してコンテキストに追加
476
+ exec(func_txt,
477
+ dict(time=time,List=List, Path=Path, argparse=argparse, common=common, options=options, logging=logging, signin=signin,),
478
+ dict(func_ctx=func_ctx))
479
+ # 関数のスキーマを生成
480
+ input_schema = dict(
481
+ type="object",
482
+ properties={o['opt']: mcp._to_schema(o, is_japan) for o in choices},
483
+ required=[],
484
+ )
485
+ output_schema = dict(type="object", properties=dict())
486
+ func_tool = FunctionTool(fn=func_ctx[0], name=func_name, title=func_name.title(), description=description,
487
+ tags=[f"mode={mode}", f"cmd={cmd}"],
488
+ parameters=input_schema, output_schema=output_schema,)
489
+ # ツールリストに追加
490
+ ret_tools.append(func_tool)
491
+
492
+ return ret_tools
493
+ return CommandListMiddleware()
494
+
495
+ def create_mw_logging(self, logger:logging.Logger, args:argparse.Namespace) -> Any:
496
+ """
497
+ ログ出力用のミドルウェアを作成します
498
+
499
+ Args:
500
+ logger (logging.Logger): ロガー
501
+ args (argparse.Namespace): 引数
502
+
503
+ Returns:
504
+ Any: ミドルウェア
505
+ """
506
+ from fastmcp.server.middleware import Middleware, MiddlewareContext
507
+ class LoggingMiddleware(Middleware):
508
+ async def on_message(self, context: MiddlewareContext, call_next):
509
+ if logger.level == logging.DEBUG:
510
+ logger.debug(f"MCP Processing method=`{context.method}`, source=`{context.source}`, message=`{context.message}`")
511
+ try:
512
+ result = await call_next(context)
513
+ if logger.level == logging.DEBUG:
514
+ logger.debug(f"MCP Complated method=`{context.method}`")
515
+ return result
516
+ except Exception as e:
517
+ logger.error(f"MCP Error method=`{context.method}`, source=`{context.source}`, message=`{context.message}`: {e}", exc_info=True)
518
+ raise e
519
+ return LoggingMiddleware()
520
+
521
+ def create_mw_reqscope(self, logger:logging.Logger, args:argparse.Namespace, web) -> Any:
522
+ """
523
+ 認証用のミドルウェアを作成します
524
+
525
+ Args:
526
+ logger (logging.Logger): ロガー
527
+ args (argparse.Namespace): 引数
528
+ web (Any): Web関連のオブジェクト
529
+
530
+ Returns:
531
+ Any: ミドルウェア
532
+ """
533
+ from fastapi import Response
534
+ from fastmcp.server.middleware import Middleware, MiddlewareContext
535
+ class ReqScopeMiddleware(Middleware):
536
+ async def on_message(self, context: MiddlewareContext, call_next):
537
+ signin.request_scope.set(dict(req=context.fastmcp_context.request_context.request, res=Response(), websocket=None, web=web))
538
+ result = await call_next(context)
539
+ return result
540
+ return ReqScopeMiddleware()
541
+
542
+ def init_agent_runner(self, logger:logging.Logger, args:argparse.Namespace, web:Any) -> Tuple[Any, Any]:
239
543
  """
240
544
  エージェントの初期化を行います
241
545
 
242
546
  Args:
243
547
  logger (logging.Logger): ロガー
244
548
  args (argparse.Namespace): 引数
549
+ web (Any): Web関連のオブジェクト
245
550
 
246
551
  Returns:
247
552
  Tuple[Any, Any]: ランナーとFastMCP
@@ -255,120 +560,11 @@ class Mcp:
255
560
  # モジュールインポート
256
561
  from fastmcp import FastMCP
257
562
  from google.adk.sessions import BaseSessionService
258
- mcp:FastMCP = self.create_mcpserver(args)
259
563
  session_service:BaseSessionService = self.create_session_service(args)
260
- options = Options.getInstance()
261
- tools:Callable[[logging.Logger, argparse.Namespace, float, Dict], Tuple[int, Dict[str, Any], Any]] = []
262
-
263
- def _ds(d:str) -> str:
264
- return f'"{d}"' if d is not None else 'None'
265
- def _t2s(o:Dict[str, Any], req=True) -> str:
266
- t, m, d, r = o["type"], o["multi"], o["default"], o["required"]
267
- if t == Options.T_BOOL: return ("List[bool]=[]" if m else f"bool={d}") if req else ("List[bool]" if m else f"bool")
268
- if t == Options.T_DATE: return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
269
- if t == Options.T_DATETIME: return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
270
- if t == Options.T_DICT: return ("List[dict]=[]" if m else f"dict={d}") if req else ("List[dict]" if m else f"dict")
271
- if t == Options.T_DIR or t == Options.T_FILE:
272
- if d is not None: d = str(d).replace('\\', '/')
273
- return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
274
- if t == Options.T_FLOAT: return ("List[float]=[]" if m else f"float={d}") if req else ("List[float]" if m else f"float")
275
- if t == Options.T_INT: return ("List[int]=[]" if m else f"int={d}") if req else ("List[int]" if m else f"int")
276
- if t == Options.T_STR: return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
277
- if t == Options.T_TEXT: return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
278
- raise ValueError(f"Unknown type: {t} for option {o['opt']}")
279
- def _arg(o:Dict[str, Any], is_japan) -> str:
280
- t, d = o["type"], o["default"]
281
- s = f' {o["opt"]}:'
282
- if t == Options.T_DIR or t == Options.T_FILE:
283
- d = str(d).replace("\\", "/")
284
- s += f'{_t2s(o, False)}={d}:'
285
- #s += f'Optional[{_t2s(o, False)}]={d}:'
286
- s += f'{o["discription_ja"] if is_japan else o["discription_en"]}'
287
- return s
288
- def _coercion(a:argparse.Namespace, key:str, dval) -> str:
289
- dval = f'opt["{key}"] if "{key}" in opt else ' + f'"{dval}"' if isinstance(dval, str) else dval
290
- aval = args.__dict__[key] if hasattr(args, key) and args.__dict__[key] else None
291
- aval = f'"{aval}"' if isinstance(aval, str) else aval
292
- ret = f'opt["{key}"] = {aval}' if aval is not None else f'opt["{key}"] = {dval}'
293
- return ret
294
- language, _ = locale.getlocale()
295
- is_japan = language.find('Japan') >= 0 or language.find('ja_JP') >= 0
296
- for mode in options.get_mode_keys():
297
- for cmd in options.get_cmd_keys(mode):
298
- if not options.get_cmd_attr(mode, cmd, 'use_agent'):
299
- continue
300
- discription = options.get_cmd_attr(mode, cmd, 'discription_ja' if is_japan else 'discription_en')
301
- choices = options.get_cmd_choices(mode, cmd, False)
302
- if len([opt for opt in choices if 'opt' in opt and opt['opt'] == 'signin_file']) <= 0:
303
- choices.append(dict(opt="signin_file", type=Options.T_FILE, default=f'.{self.ver.__appid__}/user_list.yml', required=True, multi=False, hide=True, choice=None,
304
- discription_ja="サインイン可能なユーザーとパスワードを記載したファイルを指定します。省略した時は認証を要求しません。",
305
- discription_en="Specify a file containing users and passwords with which they can signin. If omitted, no authentication is required."),)
306
- fn = f"{mode}_{cmd}"
307
- func_txt = f'def {fn}(' + ", ".join([f'{o["opt"]}:{_t2s(o, False)}' for o in choices]) + '):\n'
308
- func_txt += f' """\n'
309
- func_txt += f' {discription}\n'
310
- func_txt += f' Args:\n'
311
- func_txt += "\n".join([_arg(o, is_japan) for o in choices])
312
- func_txt += f'\n'
313
- func_txt += f' Returns:\n'
314
- func_txt += f' Dict[str, Any]:{"処理結果" if is_japan else "Processing Result"}\n'
315
- func_txt += f' """\n'
316
- func_txt += f' scope = signin.get_request_scope()\n'
317
- func_txt += f' logger = common.default_logger()\n'
318
- func_txt += f' opt = dict()\n'
319
- func_txt += f' opt["mode"] = "{mode}"\n'
320
- func_txt += f' opt["cmd"] = "{cmd}"\n'
321
- func_txt += f' opt["data"] = opt["data"] if hasattr(opt, "data") else common.HOME_DIR / ".{self.ver.__appid__}"\n'
322
- func_txt += f' opt["format"] = False\n'
323
- func_txt += f' opt["output_json"] = None\n'
324
- func_txt += f' opt["output_json_append"] = False\n'
325
- func_txt += f' opt["debug"] = logger.level == logging.DEBUG\n'
326
- func_txt += '\n'.join([f' opt["{o["opt"]}"] = {o["opt"]}' for o in choices])+'\n'
327
- func_txt += f' {_coercion(args, "host", self.default_host)}\n'
328
- func_txt += f' {_coercion(args, "port", self.default_port)}\n'
329
- func_txt += f' {_coercion(args, "password", self.default_pass)}\n'
330
- func_txt += f' {_coercion(args, "svname", self.default_svname)}\n'
331
- func_txt += f' {_coercion(args, "retry_count", 3)}\n'
332
- func_txt += f' {_coercion(args, "retry_interval", 3)}\n'
333
- func_txt += f' {_coercion(args, "timeout", 15)}\n'
334
- func_txt += f' {_coercion(args, "output_json", None)}\n'
335
- func_txt += f' {_coercion(args, "output_json_append", False)}\n'
336
- func_txt += f' {_coercion(args, "stdout_log", False)}\n'
337
- func_txt += f' {_coercion(args, "capture_stdout", False)}\n'
338
- func_txt += f' {_coercion(args, "capture_maxsize", 1024*1024)}\n'
339
- func_txt += f' {_coercion(args, "tag", None)}\n'
340
- func_txt += f' {_coercion(args, "clmsg_id", None)}\n'
341
- func_txt += f' opt["signin_file"] = signin_file if signin_file else ".{self.ver.__appid__}/user_list.yml"\n'
342
- func_txt += f' args = argparse.Namespace(**opt)\n'
343
- func_txt += f' signin_data = signin.Signin.load_signin_file(args.signin_file)\n'
344
- func_txt += f' req = scope["req"] if scope["req"] is not None else scope["websocket"]\n'
345
- func_txt += f' sign = signin.Signin._check_signin(req, scope["res"], signin_data, logger)\n'
346
- func_txt += f' if sign is not None:\n'
347
- func_txt += f' logger.warning("Unable to execute command because authentication information cannot be obtained")\n'
348
- func_txt += f' return dict(warn="Unable to execute command because authentication information cannot be obtained")\n'
349
- func_txt += f' groups = req.session["signin"]["groups"]\n'
350
- func_txt += f' logger.info("Call agent tool `{mode}_{cmd}`:user="+str(req.session["signin"]["name"])+" groups="+str(groups)+" args="+str(args))\n'
351
- func_txt += f' if not signin.Signin._check_cmd(signin_data, groups, "{mode}", "{cmd}", logger):\n'
352
- func_txt += f' logger.warning("You do not have permission to execute this command.")\n'
353
- func_txt += f' return dict(warn="You do not have permission to execute this command.")\n'
354
- func_txt += f' feat = Options.getInstance().get_cmd_attr("{mode}", "{cmd}", "feature")\n'
355
- func_txt += f' try:\n'
356
- func_txt += f' st, ret, _ = feat.apprun(logger, args, time.perf_counter(), [])\n'
357
- func_txt += f' return ret\n'
358
- func_txt += f' except Exception as e:\n'
359
- func_txt += f' logger.error("Error occurs when tool is executed:", exc_info=True)\n'
360
- func_txt += f' raise e\n'
361
- func_txt += f'tools.append({fn})\n'
362
- if logger.level == logging.DEBUG:
363
- logger.debug(f"generating agent tool: {fn}")
364
-
365
- exec(func_txt,
366
- dict(time=time,List=List, argparse=argparse, common=common, Options=Options, logging=logging, signin=signin,),
367
- dict(tools=tools, mcp=mcp))
368
- exec(f"@mcp.tool\n{func_txt}",
369
- dict(time=time,List=List, argparse=argparse, common=common, Options=Options, logging=logging, signin=signin,),
370
- dict(tools=[], mcp=mcp))
371
- root_agent = self.create_agent(logger, args, tools)
564
+ from fastmcp.tools import FunctionTool
565
+ tools:List[FunctionTool] = self.create_tools(logger, args)
566
+ mcp:FastMCP = self.create_mcpserver(args, tools, web)
567
+ root_agent = self.create_agent(logger, args, [t.fn for t in tools])
372
568
  runner = self.create_runner(logger, args, session_service, root_agent)
373
569
  if logger.level == logging.DEBUG:
374
570
  logger.debug(f"init_agent_runner complate.")
cmdbox/app/options.py CHANGED
@@ -180,7 +180,7 @@ class Options:
180
180
  o = [f'-{val["short"]}'] if "short" in val else []
181
181
  o += [f'--{key}']
182
182
  language, _ = locale.getlocale()
183
- opt['help'] = val['discription_en'] if language.find('Japan') < 0 and language.find('ja_JP') < 0 else val['discription_ja']
183
+ opt['help'] = val['description_en'] if language.find('Japan') < 0 and language.find('ja_JP') < 0 else val['description_ja']
184
184
  opt['default'] = val['default']
185
185
  if val['multi'] and val['default'] is not None:
186
186
  raise ValueError(f'list_options: The default value must be None if multi is True. key={key}, val={val}')
@@ -256,49 +256,49 @@ class Options:
256
256
  self._options = dict()
257
257
  self._options["version"] = dict(
258
258
  short="v", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True, choice=None,
259
- discription_ja="バージョン表示",
260
- discription_en="Display version")
259
+ description_ja="バージョン表示",
260
+ description_en="Display version")
261
261
  self._options["useopt"] = dict(
262
262
  short="u", type=Options.T_STR, default=None, required=False, multi=False, hide=True, choice=None,
263
- discription_ja="オプションを保存しているファイルを使用します。",
264
- discription_en="Use the file that saves the options.")
263
+ description_ja="オプションを保存しているファイルを使用します。",
264
+ description_en="Use the file that saves the options.")
265
265
  self._options["saveopt"] = dict(
266
266
  short="s", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True, choice=[True, False],
267
- discription_ja="指定しているオプションを `-u` で指定したファイルに保存します。",
268
- discription_en="Save the specified options to the file specified by `-u`.")
267
+ description_ja="指定しているオプションを `-u` で指定したファイルに保存します。",
268
+ description_en="Save the specified options to the file specified by `-u`.")
269
269
  self._options["debug"] = dict(
270
270
  short="d", type=Options.T_BOOL, default=False, required=False, multi=False, hide=True, choice=[True, False],
271
- discription_ja="デバックモードで起動します。",
272
- discription_en="Starts in debug mode.")
271
+ description_ja="デバックモードで起動します。",
272
+ description_en="Starts in debug mode.")
273
273
  self._options["format"] = dict(
274
274
  short="f", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True,
275
- discription_ja="処理結果を見やすい形式で出力します。指定しない場合json形式で出力します。",
276
- discription_en="Output the processing result in an easy-to-read format. If not specified, output in json format.",
275
+ description_ja="処理結果を見やすい形式で出力します。指定しない場合json形式で出力します。",
276
+ description_en="Output the processing result in an easy-to-read format. If not specified, output in json format.",
277
277
  choice=None)
278
278
  self._options["mode"] = dict(
279
279
  short="m", type=Options.T_STR, default=None, required=True, multi=False, hide=True,
280
- discription_ja="起動モードを指定します。",
281
- discription_en="Specify the startup mode.",
280
+ description_ja="起動モードを指定します。",
281
+ description_en="Specify the startup mode.",
282
282
  choice=[])
283
283
  self._options["cmd"] = dict(
284
284
  short="c", type=Options.T_STR, default=None, required=True, multi=False, hide=True,
285
- discription_ja="コマンドを指定します。",
286
- discription_en="Specify the command.",
285
+ description_ja="コマンドを指定します。",
286
+ description_en="Specify the command.",
287
287
  choice=[])
288
288
  self._options["tag"] = dict(
289
289
  short="t", type=Options.T_STR, default=None, required=False, multi=True, hide=True,
290
- discription_ja="このコマンドのタグを指定します。",
291
- discription_en="Specify the tag for this command.",
290
+ description_ja="このコマンドのタグを指定します。",
291
+ description_en="Specify the tag for this command.",
292
292
  choice=None)
293
293
  self._options["clmsg_id"] = dict(
294
294
  type=Options.T_STR, default=None, required=False, multi=False, hide=True,
295
- discription_ja="クライアントのメッセージIDを指定します。省略した場合はuuid4で生成されます。",
296
- discription_en="Specifies the message ID of the client. If omitted, uuid4 will be generated.",
295
+ description_ja="クライアントのメッセージIDを指定します。省略した場合はuuid4で生成されます。",
296
+ description_en="Specifies the message ID of the client. If omitted, uuid4 will be generated.",
297
297
  choice=None)
298
298
  self._options["description"] = dict(
299
299
  type=Options.T_TEXT, default=None, required=False, multi=False, hide=True,
300
- discription_ja="このコマンド登録の説明文を指定します。Agentがこのコマンドの用途を理解するのに使用します。",
301
- discription_en="Specifies a description of this command registration, used to help the Agent understand the use of this command.",
300
+ description_ja="このコマンド登録の説明文を指定します。Agentがこのコマンドの用途を理解するのに使用します。",
301
+ description_en="Specifies a description of this command registration, used to help the Agent understand the use of this command.",
302
302
  choice=None)
303
303
 
304
304
  def init_debugoption(self):
cmdbox/app/web.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from cmdbox.app import common, options
2
2
  from cmdbox.app.commons import module
3
- from fastapi import FastAPI, Request, Response
3
+ from fastapi import FastAPI, Request, Response, WebSocket
4
4
  from pathlib import Path
5
5
  from starlette.applications import Starlette
6
6
  from starlette.middleware.sessions import SessionMiddleware
@@ -34,12 +34,12 @@ class ClientTime(feature.Feature):
34
34
  """
35
35
  return dict(
36
36
  type=Options.T_STR, default=None, required=False, multi=False, hide=False, use_redis=self.USE_REDIS_FALSE,
37
- discription_ja="クライアント側の現在時刻を表示します。",
38
- discription_en="Displays the current time at the client side.",
37
+ description_ja="クライアント側の現在時刻を表示します。",
38
+ description_en="Displays the current time at the client side.",
39
39
  choice=[
40
40
  dict(opt="timedelta", type=Options.T_INT, default=9, required=False, multi=False, hide=False, choice=None,
41
- discription_ja="時差の時間数を指定します。",
42
- discription_en="Specify the number of hours of time difference."),
41
+ description_ja="時差の時間数を指定します。",
42
+ description_en="Specify the number of hours of time difference."),
43
43
  ])
44
44
 
45
45
  def apprun(self, logger:logging.Logger, args:argparse.Namespace, tm:float, pf:List[Dict[str, float]]=[]) -> Tuple[int, Dict[str, Any], Any]: