cmdbox 0.6.0.3__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (233) hide show
  1. cmdbox/app/app.py +2 -2
  2. cmdbox/app/auth/signin.py +144 -28
  3. cmdbox/app/common.py +44 -5
  4. cmdbox/app/edge.py +1 -1
  5. cmdbox/app/edge_tool.py +1 -1
  6. cmdbox/app/features/cli/agent_base.py +2 -394
  7. cmdbox/app/features/cli/cmdbox_audit_search.py +0 -2
  8. cmdbox/app/features/cli/cmdbox_audit_write.py +9 -0
  9. cmdbox/app/features/cli/cmdbox_cmd_list.py +2 -2
  10. cmdbox/app/features/cli/cmdbox_cmd_load.py +2 -2
  11. cmdbox/app/features/cli/cmdbox_mcp_proxy.py +90 -0
  12. cmdbox/app/features/cli/cmdbox_web_apikey_add.py +1 -1
  13. cmdbox/app/features/cli/cmdbox_web_apikey_del.py +1 -1
  14. cmdbox/app/features/cli/cmdbox_web_gencert.py +25 -2
  15. cmdbox/app/features/cli/cmdbox_web_group_add.py +1 -1
  16. cmdbox/app/features/cli/cmdbox_web_group_del.py +1 -1
  17. cmdbox/app/features/cli/cmdbox_web_group_edit.py +1 -1
  18. cmdbox/app/features/cli/cmdbox_web_group_list.py +1 -1
  19. cmdbox/app/features/cli/cmdbox_web_start.py +68 -26
  20. cmdbox/app/features/cli/cmdbox_web_user_add.py +1 -1
  21. cmdbox/app/features/cli/cmdbox_web_user_del.py +1 -1
  22. cmdbox/app/features/cli/cmdbox_web_user_edit.py +1 -1
  23. cmdbox/app/features/cli/cmdbox_web_user_list.py +1 -1
  24. cmdbox/app/features/web/cmdbox_web_agent.py +10 -4
  25. cmdbox/app/features/web/cmdbox_web_signin.py +1 -1
  26. cmdbox/app/mcp.py +375 -0
  27. cmdbox/app/options.py +10 -0
  28. cmdbox/app/web.py +60 -53
  29. cmdbox/autoload.py +10 -0
  30. cmdbox/extensions/user_list.yml +25 -1
  31. cmdbox/licenses/{LICENSE.Deprecated.1.2.18(MIT License).txt → LICENSE_PyJWT_2_10_1_MIT_License.txt} +2 -2
  32. cmdbox/licenses/LICENSE_exceptiongroup_1_3_0_MIT_License.txt +73 -0
  33. cmdbox/licenses/LICENSE_fastmcp_2_9_2_Apache_Software_License.txt +201 -0
  34. cmdbox/licenses/{LICENSE.graphviz.0.20.3(MIT License).txt → LICENSE_graphviz_0_21_UNKNOWN.txt} +1 -1
  35. cmdbox/licenses/LICENSE_jaraco_functools_4_2_1_UNKNOWN.txt +18 -0
  36. cmdbox/licenses/{LICENSE.numpy.2.2.5(BSD License).txt → LICENSE_numpy_2_3_1_BSD_License.txt} +8 -8
  37. cmdbox/licenses/LICENSE_openapi-pydantic_0_5_1_MIT_License.txt +40 -0
  38. cmdbox/licenses/LICENSE_propcache_0_3_2_Apache_Software_License.txt +202 -0
  39. cmdbox/licenses/LICENSE_proto-plus_1_26_1_Apache_Software_License.txt +202 -0
  40. cmdbox/licenses/LICENSE_shellingham_1_5_4_ISC_License-ISCL.txt +13 -0
  41. cmdbox/licenses/LICENSE_sphinx-last-updated-by-git_0_3_8_BSD_License.txt +22 -0
  42. cmdbox/licenses/LICENSE_tenacity_8_5_0_Apache_Software_License.txt +202 -0
  43. cmdbox/licenses/LICENSE_tokenizers_0_21_2_Apache_Software_License.txt +1 -0
  44. cmdbox/licenses/LICENSE_typer_0_16_0_MIT_License.txt +21 -0
  45. cmdbox/licenses/LICENSE_typing-inspection_0_4_1_UNKNOWN.txt +21 -0
  46. cmdbox/licenses/LICENSE_yarl_1_20_1_Apache_Software_License.txt +202 -0
  47. cmdbox/licenses/LICENSE_zipp_3_23_0_UNKNOWN.txt +18 -0
  48. cmdbox/licenses/files.txt +176 -166
  49. cmdbox/logconf_mcp.yml +43 -0
  50. cmdbox/version.py +2 -2
  51. cmdbox/web/agent.html +1 -33
  52. cmdbox/web/assets/cmdbox/agent.js +2 -3
  53. cmdbox/web/assets/cmdbox/audit.js +1 -1
  54. cmdbox/web/assets/cmdbox/common.js +73 -8
  55. cmdbox/web/assets/cmdbox/main.js +1 -2
  56. cmdbox/web/assets/cmdbox/svgicon.js +122 -0
  57. cmdbox/web/assets/cmdbox/users.js +4 -3
  58. cmdbox/web/audit.html +1 -35
  59. cmdbox/web/filer.html +1 -13
  60. cmdbox/web/gui.html +2 -50
  61. cmdbox/web/result.html +2 -46
  62. cmdbox/web/signin.html +1 -26
  63. cmdbox/web/users.html +1 -41
  64. {cmdbox-0.6.0.3.dist-info → cmdbox-0.6.1.dist-info}/METADATA +32 -7
  65. cmdbox-0.6.1.dist-info/RECORD +383 -0
  66. cmdbox/licenses/LICENSE.keyring.25.6.0(MIT License).txt +0 -17
  67. cmdbox/licenses/LICENSE.typing-inspection.0.4.0(MIT License).txt +0 -21
  68. cmdbox/licenses/LICENSE.wrapt.1.17.2(BSD License).txt +0 -24
  69. cmdbox/licenses/LICENSE.zipp.3.21.0(MIT License).txt +0 -17
  70. cmdbox-0.6.0.3.dist-info/RECORD +0 -368
  71. /cmdbox/licenses/{LICENSE.Authlib.1.5.2(BSD License).txt → LICENSE_Authlib_1_6_0_BSD_License.txt} +0 -0
  72. /cmdbox/licenses/{LICENSE.Jinja2.3.1.6(BSD License).txt → LICENSE_Jinja2_3_1_6_BSD_License.txt} +0 -0
  73. /cmdbox/licenses/{LICENSE.MarkupSafe.3.0.2(BSD License).txt → LICENSE_MarkupSafe_3_0_2_BSD_License.txt} +0 -0
  74. /cmdbox/licenses/{LICENSE.PyYAML.6.0.2(MIT License).txt → LICENSE_PyYAML_6_0_2_MIT_License.txt} +0 -0
  75. /cmdbox/licenses/{LICENSE.Pygments.2.19.1(BSD License).txt → LICENSE_Pygments_2_19_2_BSD_License.txt} +0 -0
  76. /cmdbox/licenses/{LICENSE.SQLAlchemy.2.0.40(MIT License).txt → LICENSE_SQLAlchemy_2_0_41_MIT.txt} +0 -0
  77. /cmdbox/licenses/{LICENSE.Sphinx.8.2.3(UNKNOWN).txt → LICENSE_Sphinx_8_2_3_UNKNOWN.txt} +0 -0
  78. /cmdbox/licenses/{LICENSE.aiohappyeyeballs.2.6.1(Python Software Foundation License).txt → LICENSE_aiohappyeyeballs_2_6_1_Python_Software_Foundation_License.txt} +0 -0
  79. /cmdbox/licenses/{LICENSE.aiohttp.3.11.18(Apache Software License).txt → LICENSE_aiohttp_3_12_13_Apache-2_0.txt} +0 -0
  80. /cmdbox/licenses/{LICENSE.aiosignal.1.3.2(Apache Software License).txt → LICENSE_aiosignal_1_3_2_Apache_Software_License.txt} +0 -0
  81. /cmdbox/licenses/{LICENSE.alabaster.1.0.0(BSD License).txt → LICENSE_alabaster_1_0_0_BSD_License.txt} +0 -0
  82. /cmdbox/licenses/{LICENSE.annotated-types.0.7.0(MIT License).txt → LICENSE_annotated-types_0_7_0_MIT_License.txt} +0 -0
  83. /cmdbox/licenses/{LICENSE.anyio.4.9.0(MIT License).txt → LICENSE_anyio_4_9_0_MIT_License.txt} +0 -0
  84. /cmdbox/licenses/{LICENSE.argcomplete.3.6.2(Apache Software License).txt → LICENSE_argcomplete_3_6_2_Apache_Software_License.txt} +0 -0
  85. /cmdbox/licenses/{LICENSE.async-timeout.5.0.1(Apache Software License).txt → LICENSE_async-timeout_5_0_1_Apache_Software_License.txt} +0 -0
  86. /cmdbox/licenses/{LICENSE.attrs.25.3.0(UNKNOWN).txt → LICENSE_attrs_25_3_0_UNKNOWN.txt} +0 -0
  87. /cmdbox/licenses/{LICENSE.babel.2.17.0(BSD License).txt → LICENSE_babel_2_17_0_BSD_License.txt} +0 -0
  88. /cmdbox/licenses/{LICENSE.backports.tarfile.1.2.0(MIT License).txt → LICENSE_backports_tarfile_1_2_0_MIT_License.txt} +0 -0
  89. /cmdbox/licenses/{LICENSE.cachetools.5.5.2(MIT License).txt → LICENSE_cachetools_5_5_2_MIT_License.txt} +0 -0
  90. /cmdbox/licenses/{LICENSE.certifi.2025.4.26(Mozilla Public License 2.0 (MPL 2.0)).txt → LICENSE_certifi_2025_6_15_Mozilla_Public_License_2_0-MPL_2_0.txt} +0 -0
  91. /cmdbox/licenses/{LICENSE.cffi.1.17.1(MIT License).txt → LICENSE_cffi_1_17_1_MIT_License.txt} +0 -0
  92. /cmdbox/licenses/{LICENSE.charset-normalizer.3.4.2(MIT License).txt → LICENSE_charset-normalizer_3_4_2_MIT_License.txt} +0 -0
  93. /cmdbox/licenses/{LICENSE.click.8.2.0(UNKNOWN).txt → LICENSE_click_8_2_1_UNKNOWN.txt} +0 -0
  94. /cmdbox/licenses/{LICENSE.jiter.0.9.0(MIT License).txt → LICENSE_cloudpickle_3_1_1_BSD_License.txt} +0 -0
  95. /cmdbox/licenses/{LICENSE.colorama.0.4.6(BSD License).txt → LICENSE_colorama_0_4_6_BSD_License.txt} +0 -0
  96. /cmdbox/licenses/{LICENSE.cryptography.44.0.3(Apache Software License; BSD License).txt → LICENSE_cryptography_45_0_4_Apache-2_0_OR_BSD-3-Clause.txt} +0 -0
  97. /cmdbox/licenses/{LICENSE.distro.1.9.0(Apache Software License).txt → LICENSE_distro_1_9_0_Apache_Software_License.txt} +0 -0
  98. /cmdbox/licenses/{LICENSE.docstring_parser.0.16(MIT License).txt → LICENSE_docstring_parser_0_16_MIT_License.txt} +0 -0
  99. /cmdbox/licenses/{LICENSE.docutils.0.21.2(BSD License; GNU General Public License (GPL); Public Domain; Python Software Foundation License).txt → LICENSE_docutils_0_21_2_BSD_License-GNU_General_Public_License-GPL-Public_Domain-Python_Software_Foundation_License.txt} +0 -0
  100. /cmdbox/licenses/{LICENSE.fastapi.0.115.12(MIT License).txt → LICENSE_fastapi_0_115_14_MIT_License.txt} +0 -0
  101. /cmdbox/licenses/{LICENSE.filelock.3.18.0(The Unlicense (Unlicense)).txt → LICENSE_filelock_3_18_0_The_Unlicense-Unlicense.txt} +0 -0
  102. /cmdbox/licenses/{LICENSE.frozenlist.1.6.0(Apache-2.0).txt → LICENSE_frozenlist_1_7_0_Apache-2_0.txt} +0 -0
  103. /cmdbox/licenses/{LICENSE.fsspec.2025.3.2(BSD License).txt → LICENSE_fsspec_2025_5_1_BSD_License.txt} +0 -0
  104. /cmdbox/licenses/{LICENSE.gevent.25.4.2(MIT).txt → LICENSE_gevent_25_5_1_MIT.txt} +0 -0
  105. /cmdbox/licenses/{LICENSE.google-adk.0.5.0(Apache Software License).txt → LICENSE_google-adk_1_5_0_Apache_Software_License.txt} +0 -0
  106. /cmdbox/licenses/{LICENSE.google-api-core.2.24.2(Apache Software License).txt → LICENSE_google-api-core_2_25_1_Apache_Software_License.txt} +0 -0
  107. /cmdbox/licenses/{LICENSE.google-api-python-client.2.169.0(Apache Software License).txt → LICENSE_google-api-python-client_2_174_0_Apache_Software_License.txt} +0 -0
  108. /cmdbox/licenses/{LICENSE.google-auth-httplib2.0.2.0(Apache Software License).txt → LICENSE_google-auth-httplib2_0_2_0_Apache_Software_License.txt} +0 -0
  109. /cmdbox/licenses/{LICENSE.google-auth.2.40.1(Apache Software License).txt → LICENSE_google-auth_2_40_3_Apache_Software_License.txt} +0 -0
  110. /cmdbox/licenses/{LICENSE.google-cloud-aiplatform.1.92.0(Apache 2.0).txt → LICENSE_google-cloud-aiplatform_1_100_0_Apache_2_0.txt} +0 -0
  111. /cmdbox/licenses/{LICENSE.google-cloud-bigquery.3.31.0(Apache Software License).txt → LICENSE_google-cloud-appengine-logging_1_6_2_Apache_Software_License.txt} +0 -0
  112. /cmdbox/licenses/{LICENSE.google-cloud-core.2.4.3(Apache Software License).txt → LICENSE_google-cloud-audit-log_0_3_2_Apache_Software_License.txt} +0 -0
  113. /cmdbox/licenses/{LICENSE.google-cloud-resource-manager.1.14.2(Apache Software License).txt → LICENSE_google-cloud-bigquery_3_34_0_Apache_Software_License.txt} +0 -0
  114. /cmdbox/licenses/{LICENSE.google-cloud-secret-manager.2.23.3(Apache Software License).txt → LICENSE_google-cloud-core_2_4_3_Apache_Software_License.txt} +0 -0
  115. /cmdbox/licenses/{LICENSE.google-cloud-speech.2.32.0(Apache Software License).txt → LICENSE_google-cloud-logging_3_12_1_Apache_Software_License.txt} +0 -0
  116. /cmdbox/licenses/{LICENSE.google-cloud-storage.2.19.0(Apache Software License).txt → LICENSE_google-cloud-resource-manager_1_14_2_Apache_Software_License.txt} +0 -0
  117. /cmdbox/licenses/{LICENSE.google-cloud-trace.1.16.1(Apache Software License).txt → LICENSE_google-cloud-secret-manager_2_24_0_Apache_Software_License.txt} +0 -0
  118. /cmdbox/licenses/{LICENSE.google-crc32c.1.7.1(Apache 2.0).txt → LICENSE_google-cloud-speech_2_33_0_Apache_Software_License.txt} +0 -0
  119. /cmdbox/licenses/{LICENSE.google-genai.1.14.0(Apache Software License).txt → LICENSE_google-cloud-storage_2_19_0_Apache_Software_License.txt} +0 -0
  120. /cmdbox/licenses/{LICENSE.google-resumable-media.2.7.2(Apache Software License).txt → LICENSE_google-cloud-trace_1_16_2_Apache_Software_License.txt} +0 -0
  121. /cmdbox/licenses/{LICENSE.googleapis-common-protos.1.70.0(Apache Software License).txt → LICENSE_google-crc32c_1_7_1_Apache_2_0.txt} +0 -0
  122. /cmdbox/licenses/{LICENSE.grpc-google-iam-v1.0.14.2(Apache Software License).txt → LICENSE_google-genai_1_23_0_Apache_Software_License.txt} +0 -0
  123. /cmdbox/licenses/{LICENSE.id.1.5.0(Apache Software License).txt → LICENSE_google-resumable-media_2_7_2_Apache_Software_License.txt} +0 -0
  124. /cmdbox/licenses/{LICENSE.importlib_metadata.8.6.1(Apache Software License).txt → LICENSE_googleapis-common-protos_1_70_0_Apache_Software_License.txt} +0 -0
  125. /cmdbox/licenses/{LICENSE.greenlet.3.2.2(MIT AND Python-2.0).txt → LICENSE_greenlet_3_2_3_MIT_AND_Python-2_0.txt} +0 -0
  126. /cmdbox/licenses/{LICENSE.propcache.0.3.1(Apache Software License).txt → LICENSE_grpc-google-iam-v1_0_14_2_Apache_Software_License.txt} +0 -0
  127. /cmdbox/licenses/{LICENSE.grpcio-status.1.71.0(Apache Software License).txt → LICENSE_grpcio-status_1_73_1_Apache_Software_License.txt} +0 -0
  128. /cmdbox/licenses/{LICENSE.grpcio.1.71.0(Apache Software License).txt → LICENSE_grpcio_1_73_1_Apache_Software_License.txt} +0 -0
  129. /cmdbox/licenses/{LICENSE.gunicorn.23.0.0(MIT License).txt → LICENSE_gunicorn_23_0_0_MIT_License.txt} +0 -0
  130. /cmdbox/licenses/{LICENSE.h11.0.16.0(MIT License).txt → LICENSE_h11_0_16_0_MIT_License.txt} +0 -0
  131. /cmdbox/licenses/{LICENSE.httpcore.1.0.9(BSD License).txt → LICENSE_httpcore_1_0_9_BSD_License.txt} +0 -0
  132. /cmdbox/licenses/{LICENSE.httplib2.0.22.0(MIT License).txt → LICENSE_httplib2_0_22_0_MIT_License.txt} +0 -0
  133. /cmdbox/licenses/{LICENSE.httptools.0.6.4(MIT License).txt → LICENSE_httptools_0_6_4_MIT_License.txt} +0 -0
  134. /cmdbox/licenses/{LICENSE.httpx-sse.0.4.0(MIT).txt → LICENSE_httpx-sse_0_4_1_MIT.txt} +0 -0
  135. /cmdbox/licenses/{LICENSE.httpx.0.28.1(BSD License).txt → LICENSE_httpx_0_28_1_BSD_License.txt} +0 -0
  136. /cmdbox/licenses/{LICENSE.huggingface-hub.0.31.1(Apache Software License).txt → LICENSE_huggingface-hub_0_33_1_Apache_Software_License.txt} +0 -0
  137. /cmdbox/licenses/{LICENSE.proto-plus.1.26.1(Apache Software License).txt → LICENSE_id_1_5_0_Apache_Software_License.txt} +0 -0
  138. /cmdbox/licenses/{LICENSE.idna.3.10(BSD License).txt → LICENSE_idna_3_10_BSD_License.txt} +0 -0
  139. /cmdbox/licenses/{LICENSE.imagesize.1.4.1(MIT License).txt → LICENSE_imagesize_1_4_1_MIT_License.txt} +0 -0
  140. /cmdbox/licenses/{LICENSE.yarl.1.20.0(Apache Software License).txt → LICENSE_importlib_metadata_8_7_0_Apache_Software_License.txt} +0 -0
  141. /cmdbox/licenses/{LICENSE.itsdangerous.2.2.0(BSD License).txt → LICENSE_itsdangerous_2_2_0_BSD_License.txt} +0 -0
  142. /cmdbox/licenses/{LICENSE.jaraco.classes.3.4.0(MIT License).txt → LICENSE_jaraco_classes_3_4_0_MIT_License.txt} +0 -0
  143. /cmdbox/licenses/{LICENSE.jaraco.context.6.0.1(MIT License).txt → LICENSE_jaraco_context_6_0_1_MIT_License.txt} +0 -0
  144. /cmdbox/licenses/{LICENSE.sphinxcontrib-applehelp.2.0.0(BSD License).txt → LICENSE_jiter_0_10_0_MIT_License.txt} +0 -0
  145. /cmdbox/licenses/{LICENSE.jsonschema-specifications.2025.4.1(UNKNOWN).txt → LICENSE_jsonschema-specifications_2025_4_1_UNKNOWN.txt} +0 -0
  146. /cmdbox/licenses/{LICENSE.jsonschema.4.23.0(MIT License).txt → LICENSE_jsonschema_4_24_0_UNKNOWN.txt} +0 -0
  147. /cmdbox/licenses/{LICENSE.jaraco.functools.4.1.0(MIT License).txt → LICENSE_keyring_25_6_0_MIT_License.txt} +0 -0
  148. /cmdbox/licenses/{LICENSE.litellm.1.69.0(MIT License).txt → LICENSE_litellm_1_73_6_MIT_License.txt} +0 -0
  149. /cmdbox/licenses/{LICENSE.markdown-it-py.3.0.0(MIT License).txt → LICENSE_markdown-it-py_3_0_0_MIT_License.txt} +0 -0
  150. /cmdbox/licenses/{LICENSE.mcp.1.8.0(MIT License).txt → LICENSE_mcp_1_9_4_MIT_License.txt} +0 -0
  151. /cmdbox/licenses/{LICENSE.mdurl.0.1.2(MIT License).txt → LICENSE_mdurl_0_1_2_MIT_License.txt} +0 -0
  152. /cmdbox/licenses/{LICENSE.more-itertools.10.7.0(MIT License).txt → LICENSE_more-itertools_10_7_0_MIT_License.txt} +0 -0
  153. /cmdbox/licenses/{LICENSE.multidict.6.4.3(Apache Software License).txt → LICENSE_multidict_6_6_2_Apache_License_2_0.txt} +0 -0
  154. /cmdbox/licenses/{LICENSE.nh3.0.2.21(MIT).txt → LICENSE_nh3_0_2_21_MIT.txt} +0 -0
  155. /cmdbox/licenses/{LICENSE.openai.1.75.0(Apache Software License).txt → LICENSE_openai_1_93_0_Apache_Software_License.txt} +0 -0
  156. /cmdbox/licenses/{LICENSE.opentelemetry-api.1.33.0(Apache Software License).txt → LICENSE_opentelemetry-api_1_34_1_Apache_Software_License.txt} +0 -0
  157. /cmdbox/licenses/{LICENSE.opentelemetry-exporter-gcp-trace.1.9.0(Apache Software License).txt → LICENSE_opentelemetry-exporter-gcp-trace_1_9_0_Apache_Software_License.txt} +0 -0
  158. /cmdbox/licenses/{LICENSE.opentelemetry-resourcedetector-gcp.1.9.0a0(Apache Software License).txt → LICENSE_opentelemetry-resourcedetector-gcp_1_9_0a0_Apache_Software_License.txt} +0 -0
  159. /cmdbox/licenses/{LICENSE.opentelemetry-sdk.1.33.0(Apache Software License).txt → LICENSE_opentelemetry-sdk_1_34_1_Apache_Software_License.txt} +0 -0
  160. /cmdbox/licenses/{LICENSE.opentelemetry-semantic-conventions.0.54b0(Apache Software License).txt → LICENSE_opentelemetry-semantic-conventions_0_55b1_Apache_Software_License.txt} +0 -0
  161. /cmdbox/licenses/{LICENSE.packaging.25.0(Apache Software License; BSD License).txt → LICENSE_packaging_25_0_Apache_Software_License-BSD_License.txt} +0 -0
  162. /cmdbox/licenses/{LICENSE.pillow.11.2.1(UNKNOWN).txt → LICENSE_pillow_11_2_1_UNKNOWN.txt} +0 -0
  163. /cmdbox/licenses/{LICENSE.pip.24.0(MIT License).txt → LICENSE_pip_24_0_MIT_License.txt} +0 -0
  164. /cmdbox/licenses/{LICENSE.plyer.2.1.0(MIT License).txt → LICENSE_plyer_2_1_0_MIT_License.txt} +0 -0
  165. /cmdbox/licenses/{LICENSE.prettytable.3.16.0(UNKNOWN).txt → LICENSE_prettytable_3_16_0_UNKNOWN.txt} +0 -0
  166. /cmdbox/licenses/{LICENSE.prompt_toolkit.3.0.51(BSD License).txt → LICENSE_prompt_toolkit_3_0_51_BSD_License.txt} +0 -0
  167. /cmdbox/licenses/{LICENSE.protobuf.5.29.4(3-Clause BSD License).txt → LICENSE_protobuf_6_31_1_3-Clause_BSD_License.txt} +0 -0
  168. /cmdbox/licenses/{LICENSE.psycopg-binary.3.2.7(GNU Lesser General Public License v3 (LGPLv3)).txt → LICENSE_psycopg-binary_3_2_9_GNU_Lesser_General_Public_License_v3-LGPLv3.txt} +0 -0
  169. /cmdbox/licenses/{LICENSE.psycopg.3.2.7(GNU Lesser General Public License v3 (LGPLv3)).txt → LICENSE_psycopg_3_2_9_GNU_Lesser_General_Public_License_v3-LGPLv3.txt} +0 -0
  170. /cmdbox/licenses/{LICENSE.pyasn1.0.6.1(BSD License).txt → LICENSE_pyasn1_0_6_1_BSD_License.txt} +0 -0
  171. /cmdbox/licenses/{LICENSE.pyasn1_modules.0.4.2(BSD License).txt → LICENSE_pyasn1_modules_0_4_2_BSD_License.txt} +0 -0
  172. /cmdbox/licenses/{LICENSE.pycparser.2.22(BSD License).txt → LICENSE_pycparser_2_22_BSD_License.txt} +0 -0
  173. /cmdbox/licenses/{LICENSE.pycryptodome.3.22.0(BSD License; Public Domain).txt → LICENSE_pycryptodome_3_23_0_BSD_License-Public_Domain.txt} +0 -0
  174. /cmdbox/licenses/{LICENSE.pydantic-settings.2.9.1(MIT License).txt → LICENSE_pydantic-settings_2_10_1_MIT_License.txt} +0 -0
  175. /cmdbox/licenses/{LICENSE.pydantic.2.11.4(MIT License).txt → LICENSE_pydantic_2_11_7_MIT_License.txt} +0 -0
  176. /cmdbox/licenses/{LICENSE.pydantic_core.2.33.2(MIT License).txt → LICENSE_pydantic_core_2_33_2_MIT_License.txt} +0 -0
  177. /cmdbox/licenses/{LICENSE.pyparsing.3.2.3(MIT License).txt → LICENSE_pyparsing_3_2_3_MIT_License.txt} +0 -0
  178. /cmdbox/licenses/{LICENSE.pystray.0.19.5(GNU Lesser General Public License v3 (LGPLv3)).txt → LICENSE_pystray_0_19_5_GNU_Lesser_General_Public_License_v3-LGPLv3.txt} +0 -0
  179. /cmdbox/licenses/{LICENSE.python-dateutil.2.9.0.post0(Apache Software License; BSD License).txt → LICENSE_python-dateutil_2_9_0_post0_Apache_Software_License-BSD_License.txt} +0 -0
  180. /cmdbox/licenses/{LICENSE.python-dotenv.1.1.0(BSD License).txt → LICENSE_python-dotenv_1_1_1_BSD_License.txt} +0 -0
  181. /cmdbox/licenses/{LICENSE.python-multipart.0.0.20(Apache Software License).txt → LICENSE_python-multipart_0_0_20_Apache_Software_License.txt} +0 -0
  182. /cmdbox/licenses/{LICENSE.pywin32-ctypes.0.2.3(BSD-3-Clause).txt → LICENSE_pywin32-ctypes_0_2_3_BSD-3-Clause.txt} +0 -0
  183. /cmdbox/licenses/{LICENSE.questionary.2.1.0(MIT License).txt → LICENSE_questionary_2_1_0_MIT_License.txt} +0 -0
  184. /cmdbox/licenses/{LICENSE.readme_renderer.44.0(Apache Software License).txt → LICENSE_readme_renderer_44_0_Apache_Software_License.txt} +0 -0
  185. /cmdbox/licenses/{LICENSE.redis.6.0.0(MIT License).txt → LICENSE_redis_6_2_0_MIT_License.txt} +0 -0
  186. /cmdbox/licenses/{LICENSE.referencing.0.36.2(UNKNOWN).txt → LICENSE_referencing_0_36_2_UNKNOWN.txt} +0 -0
  187. /cmdbox/licenses/{LICENSE.regex.2024.11.6(Apache Software License).txt → LICENSE_regex_2024_11_6_Apache_Software_License.txt} +0 -0
  188. /cmdbox/licenses/{LICENSE.requests-toolbelt.1.0.0(Apache Software License).txt → LICENSE_requests-toolbelt_1_0_0_Apache_Software_License.txt} +0 -0
  189. /cmdbox/licenses/{LICENSE.requests.2.32.3(Apache Software License).txt → LICENSE_requests_2_32_4_Apache_Software_License.txt} +0 -0
  190. /cmdbox/licenses/{LICENSE.rfc3986.2.0.0(Apache Software License).txt → LICENSE_rfc3986_2_0_0_Apache_Software_License.txt} +0 -0
  191. /cmdbox/licenses/{LICENSE.rich.14.0.0(MIT License).txt → LICENSE_rich_14_0_0_MIT_License.txt} +0 -0
  192. /cmdbox/licenses/{LICENSE.roman-numerals-py.3.1.0(CC0 1.0 Universal (CC0 1.0) Public Domain Dedication; Zero-Clause BSD (0BSD)).txt → LICENSE_roman-numerals-py_3_1_0_CC0_1_0_Universal-CC0_1_0-Public_Domain_Dedication-Zero-Clause_BSD-0BSD.txt} +0 -0
  193. /cmdbox/licenses/{LICENSE.rpds-py.0.24.0(MIT).txt → LICENSE_rpds-py_0_25_1_MIT.txt} +0 -0
  194. /cmdbox/licenses/{LICENSE.rsa.4.9.1(Apache Software License).txt → LICENSE_rsa_4_9_1_Apache_Software_License.txt} +0 -0
  195. /cmdbox/licenses/{LICENSE.setuptools.65.5.0(MIT License).txt → LICENSE_setuptools_65_5_0_MIT_License.txt} +0 -0
  196. /cmdbox/licenses/{LICENSE.shapely.2.1.0(BSD License).txt → LICENSE_shapely_2_1_1_BSD_License.txt} +0 -0
  197. /cmdbox/licenses/{LICENSE.six.1.17.0(MIT License).txt → LICENSE_six_1_17_0_MIT_License.txt} +0 -0
  198. /cmdbox/licenses/{LICENSE.sniffio.1.3.1(Apache Software License; MIT License).txt → LICENSE_sniffio_1_3_1_Apache_Software_License-MIT_License.txt} +0 -0
  199. /cmdbox/licenses/{LICENSE.snowballstemmer.3.0.1(BSD License).txt → LICENSE_snowballstemmer_3_0_1_BSD_License.txt} +0 -0
  200. /cmdbox/licenses/{LICENSE.sphinx-intl.2.3.1(BSD License).txt → LICENSE_sphinx-intl_2_3_1_BSD_License.txt} +0 -0
  201. /cmdbox/licenses/{LICENSE.sphinx-rtd-theme.3.0.2(MIT License).txt → LICENSE_sphinx-rtd-theme_3_0_2_MIT_License.txt} +0 -0
  202. /cmdbox/licenses/{LICENSE.sphinx-sitemap.2.6.0(MIT License).txt → LICENSE_sphinx-sitemap_2_7_2_UNKNOWN.txt} +0 -0
  203. /cmdbox/licenses/{LICENSE.sphinx_fontawesome.0.0.6(GNU General Public License v2 (GPLv2)).txt → LICENSE_sphinx_fontawesome_0_0_6_GNU_General_Public_License_v2-GPLv2.txt} +0 -0
  204. /cmdbox/licenses/{LICENSE.sphinxcontrib-devhelp.2.0.0(BSD License).txt → LICENSE_sphinxcontrib-applehelp_2_0_0_BSD_License.txt} +0 -0
  205. /cmdbox/licenses/{LICENSE.sphinxcontrib-htmlhelp.2.1.0(BSD License).txt → LICENSE_sphinxcontrib-devhelp_2_0_0_BSD_License.txt} +0 -0
  206. /cmdbox/licenses/{LICENSE.sphinxcontrib-jquery.4.1(BSD License).txt → LICENSE_sphinxcontrib-htmlhelp_2_1_0_BSD_License.txt} +0 -0
  207. /cmdbox/licenses/{LICENSE.sphinxcontrib-qthelp.2.0.0(BSD License).txt → LICENSE_sphinxcontrib-jquery_4_1_BSD_License.txt} +0 -0
  208. /cmdbox/licenses/{LICENSE.sphinxcontrib-jsmath.1.0.1(BSD License).txt → LICENSE_sphinxcontrib-jsmath_1_0_1_BSD_License.txt} +0 -0
  209. /cmdbox/licenses/{LICENSE.sphinxcontrib-serializinghtml.2.0.0(BSD License).txt → LICENSE_sphinxcontrib-qthelp_2_0_0_BSD_License.txt} +0 -0
  210. /cmdbox/licenses/{LICENSE.tokenizers.0.21.1(Apache Software License).txt → LICENSE_sphinxcontrib-serializinghtml_2_0_0_BSD_License.txt} +0 -0
  211. /cmdbox/licenses/{LICENSE.sse-starlette.2.3.4(BSD License).txt → LICENSE_sse-starlette_2_3_6_BSD_License.txt} +0 -0
  212. /cmdbox/licenses/{LICENSE.starlette.0.46.2(BSD License).txt → LICENSE_starlette_0_46_2_BSD_License.txt} +0 -0
  213. /cmdbox/licenses/{LICENSE.tabulate.0.9.0(MIT License).txt → LICENSE_tabulate_0_9_0_MIT_License.txt} +0 -0
  214. /cmdbox/licenses/{LICENSE.tiktoken.0.9.0(MIT License).txt → LICENSE_tiktoken_0_9_0_MIT_License-Copyright-c-2022_OpenAI-Shantanu_Jain-Permission_is_hereby_granted-free_of_charge-to_any_pers.txt} +0 -0
  215. /cmdbox/licenses/{LICENSE.tomli.2.2.1(MIT License).txt → LICENSE_tomli_2_2_1_MIT_License.txt} +0 -0
  216. /cmdbox/licenses/{LICENSE.tqdm.4.67.1(MIT License; Mozilla Public License 2.0 (MPL 2.0)).txt → LICENSE_tqdm_4_67_1_MIT_License-Mozilla_Public_License_2_0-MPL_2_0.txt} +0 -0
  217. /cmdbox/licenses/{LICENSE.twine.6.1.0(Apache Software License).txt → LICENSE_twine_6_1_0_Apache_Software_License.txt} +0 -0
  218. /cmdbox/licenses/{LICENSE.typing_extensions.4.13.2(UNKNOWN).txt → LICENSE_typing_extensions_4_14_0_UNKNOWN.txt} +0 -0
  219. /cmdbox/licenses/{LICENSE.tzdata.2025.2(Apache Software License).txt → LICENSE_tzdata_2025_2_Apache_Software_License.txt} +0 -0
  220. /cmdbox/licenses/{LICENSE.tzlocal.5.3.1(MIT License).txt → LICENSE_tzlocal_5_3_1_MIT_License.txt} +0 -0
  221. /cmdbox/licenses/{LICENSE.uritemplate.4.1.1(Apache Software License; BSD License).txt → LICENSE_uritemplate_4_2_0_BSD_3-Clause_OR_Apache-2_0.txt} +0 -0
  222. /cmdbox/licenses/{LICENSE.urllib3.2.4.0(UNKNOWN).txt → LICENSE_urllib3_2_5_0_UNKNOWN.txt} +0 -0
  223. /cmdbox/licenses/{LICENSE.uvicorn.0.34.2(BSD License).txt → LICENSE_uvicorn_0_35_0_BSD_License.txt} +0 -0
  224. /cmdbox/licenses/{LICENSE.watchfiles.1.0.5(MIT License).txt → LICENSE_watchfiles_1_1_0_MIT_License.txt} +0 -0
  225. /cmdbox/licenses/{LICENSE.wcwidth.0.2.13(MIT License).txt → LICENSE_wcwidth_0_2_13_MIT_License.txt} +0 -0
  226. /cmdbox/licenses/{LICENSE.websockets.15.0.1(BSD License).txt → LICENSE_websockets_15_0_1_BSD_License.txt} +0 -0
  227. /cmdbox/licenses/{LICENSE.wheel.0.45.1(MIT License).txt → LICENSE_wheel_0_45_1_MIT_License.txt} +0 -0
  228. /cmdbox/licenses/{LICENSE.zope.event.5.0(Zope Public License).txt → LICENSE_zope_event_5_1_Zope_Public_License.txt} +0 -0
  229. /cmdbox/licenses/{LICENSE.zope.interface.7.2(Zope Public License).txt → LICENSE_zope_interface_7_2_Zope_Public_License.txt} +0 -0
  230. {cmdbox-0.6.0.3.dist-info → cmdbox-0.6.1.dist-info}/LICENSE +0 -0
  231. {cmdbox-0.6.0.3.dist-info → cmdbox-0.6.1.dist-info}/WHEEL +0 -0
  232. {cmdbox-0.6.0.3.dist-info → cmdbox-0.6.1.dist-info}/entry_points.txt +0 -0
  233. {cmdbox-0.6.0.3.dist-info → cmdbox-0.6.1.dist-info}/top_level.txt +0 -0
cmdbox/app/mcp.py ADDED
@@ -0,0 +1,375 @@
1
+ from cmdbox.app import common, options
2
+ from cmdbox.app.options import Options
3
+ from cmdbox.app.auth import signin
4
+ from pathlib import Path
5
+ from typing import Callable, List, Dict, Any, Tuple
6
+ import argparse
7
+ import logging
8
+ import locale
9
+ import json
10
+ import time
11
+ import re
12
+ import os
13
+
14
+
15
+ class Mcp:
16
+ default_host:str = os.environ.get('REDIS_HOST', 'localhost')
17
+ default_port:int = int(os.environ.get('REDIS_PORT', '6379'))
18
+ default_pass:str = os.environ.get('REDIS_PASSWORD', 'password')
19
+ default_svname:str = os.environ.get('SVNAME', 'server')
20
+
21
+ def __init__(self, logger:logging.Logger, data:Path, sign:signin.Signin, appcls=None, ver=None,):
22
+ """
23
+ MCP (Multi-Channel Protocol) クラスの初期化
24
+
25
+ Args:
26
+ logger (logging.Logger): ロガー
27
+ data (Path): データのパス
28
+ sign (signin.Signin): サインインオブジェクト
29
+ appcls (type, optional): アプリケーションクラス. Defaults to None.
30
+ ver (module, optional): バージョンモジュール. Defaults to None.
31
+ """
32
+ self.logger = logger
33
+ self.data = data
34
+ self.appcls = appcls
35
+ self.ver = ver
36
+ self.signin = sign
37
+
38
+ def create_mcpserver(self, args:argparse.Namespace) -> Any:
39
+ """
40
+ mcpserverを作成します
41
+
42
+ Args:
43
+ args (argparse.Namespace): 引数
44
+
45
+ Returns:
46
+ Any: FastMCP
47
+ """
48
+ from fastmcp import FastMCP
49
+ from fastmcp.server.auth import BearerAuthProvider
50
+ cls = self.signin.__class__
51
+ publickey_str = cls.verify_jwt_publickey_str if hasattr(cls, 'verify_jwt_publickey_str') else None
52
+ issuer = cls.verify_jwt_issuer if hasattr(cls, 'verify_jwt_issuer') else None
53
+ audience = cls.verify_jwt_audience if hasattr(cls, 'verify_jwt_audience') else None
54
+ if publickey_str is not None and issuer is not None and audience is not None:
55
+ self.logger.info(f"Using BearerAuthProvider with public key, issuer: {issuer}, audience: {audience}")
56
+ auth = BearerAuthProvider(
57
+ public_key=publickey_str,
58
+ issuer=issuer,
59
+ audience=audience
60
+ )
61
+ mcp = FastMCP(name=self.ver.__appid__, auth=auth)
62
+ else:
63
+ self.logger.info(f"Using BearerAuthProvider without public key, issuer, or audience.")
64
+ mcp = FastMCP(name=self.ver.__appid__)
65
+ return mcp
66
+
67
+ def create_session_service(self, args:argparse.Namespace) -> Any:
68
+ """
69
+ セッションサービスを作成します
70
+
71
+ Args:
72
+ args (argparse.Namespace): 引数
73
+
74
+ Returns:
75
+ BaseSessionService: セッションサービス
76
+ """
77
+ from google.adk.events import Event
78
+ from google.adk.sessions import DatabaseSessionService, InMemorySessionService, session
79
+ from typing_extensions import override
80
+ if hasattr(args, 'agent_session_dburl') and args.agent_session_dburl is not None:
81
+ class _DatabaseSessionService(DatabaseSessionService):
82
+ @override
83
+ async def append_event(self, session: session.Session, event: Event) -> Event:
84
+ # 永続化されるセッションには <important> タグを含めない
85
+ bk_parts = event.content.parts.copy()
86
+ for part in event.content.parts:
87
+ if not part.text: continue
88
+ part.text = re.sub(r"<important>.*</important>", "", part.text)
89
+ for part in bk_parts:
90
+ if not part.text: continue
91
+ part.text = part.text.replace("<important>", "").replace("</important>", "")
92
+ ret = await super().append_event(session, event)
93
+ ret.content.parts = bk_parts
94
+ return ret
95
+ dss = _DatabaseSessionService(db_url=args.agent_session_dburl)
96
+ #dss.db_engine.echo = True
97
+ return dss
98
+ else:
99
+ return InMemorySessionService()
100
+
101
+ def create_agent(self, logger:logging.Logger, args:argparse.Namespace, tools:List[Callable]) -> Any:
102
+ """
103
+ エージェントを作成します
104
+
105
+ Args:
106
+ logger (logging.Logger): ロガー
107
+ args (argparse.Namespace): 引数
108
+ tools (List[Callable]): 関数
109
+
110
+ Returns:
111
+ Agent: エージェント
112
+ """
113
+ if logger.level == logging.DEBUG:
114
+ logger.debug(f"create_agent processing..")
115
+ language, _ = locale.getlocale()
116
+ is_japan = language.find('Japan') >= 0 or language.find('ja_JP') >= 0
117
+ description = f"{self.ver.__appid__}に登録されているコマンド提供"
118
+ instruction = f"あなたはコマンドの意味を熟知しているエキスパートです。" + \
119
+ f"ユーザーがコマンドを実行したいとき、あなたは以下の手順に従ってコマンドを確実に実行してください。\n" + \
120
+ f"1. ユーザーのクエリからが実行したいコマンドを特定します。\n" + \
121
+ f"2. コマンド実行に必要なパラメータのなかで、ユーザーのクエリから取得できないものは、コマンド定義にあるデフォルト値を指定して実行してください。\n" + \
122
+ f"3. もしエラーが発生した場合は、ユーザーにコマンド名とパラメータとエラー内容を提示してください。\n"
123
+
124
+ description = description if is_japan else \
125
+ f"Command offer registered in {self.ver.__appid__}."
126
+ instruction = instruction if is_japan else \
127
+ f"You are the expert who knows what the commands mean." + \
128
+ f"When a user wants to execute a command, you follow these steps to ensure that the command is executed.\n" + \
129
+ f"1. Identify the command you want to execute from the user's query.\n" + \
130
+ 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"
132
+
133
+ description = args.agent_description if args.agent_description else description
134
+ instruction = args.agent_instruction if args.agent_instruction else instruction
135
+ if logger.level == logging.DEBUG:
136
+ logger.debug(f"google-adk loading..")
137
+ from google.adk.agents import Agent
138
+ if logger.level == logging.DEBUG:
139
+ logger.debug(f"litellm loading..")
140
+ from google.adk.models.lite_llm import LiteLlm
141
+ # loggerの初期化
142
+ common.reset_logger("LiteLLM Proxy")
143
+ common.reset_logger("LiteLLM Router")
144
+ common.reset_logger("LiteLLM")
145
+ if args.llmprov == 'openai':
146
+ if args.llmmodel is None: raise ValueError("llmmodel is required.")
147
+ if args.llmapikey is None: raise ValueError("llmapikey is required.")
148
+ agent = Agent(
149
+ name=args.agent_name,
150
+ model=LiteLlm(
151
+ model=args.llmmodel,
152
+ api_key=args.llmapikey,
153
+ endpoint=args.llmendpoint,
154
+ ),
155
+ description=description,
156
+ instruction=instruction,
157
+ tools=tools,
158
+ )
159
+ elif args.llmprov == 'azureopenai':
160
+ if args.llmmodel is None: raise ValueError("llmmodel is required.")
161
+ if args.llmendpoint is None: raise ValueError("llmendpoint is required.")
162
+ if args.llmapikey is None: raise ValueError("llmapikey is required.")
163
+ if args.llmapiversion is None: raise ValueError("llmapiversion is required.")
164
+ agent = Agent(
165
+ name=args.agent_name,
166
+ model=LiteLlm(
167
+ model=args.llmmodel,
168
+ api_key=args.llmapikey,
169
+ endpoint=args.llmendpoint,
170
+ api_version=args.llmapiversion,
171
+ ),
172
+ description=description,
173
+ instruction=instruction,
174
+ tools=tools,
175
+ )
176
+ elif args.llmprov == 'vertexai':
177
+ if args.llmmodel is None: raise ValueError("llmmodel is required.")
178
+ if args.llmlocation is None: raise ValueError("llmlocation is required.")
179
+ if args.llmsvaccountfile is not None:
180
+ with open(args.llmsvaccountfile, "r", encoding="utf-8") as f:
181
+ vertex_credentials = json.load(f)
182
+ elif args.llmprojectid is None: raise ValueError("llmprojectid is required.")
183
+ agent = Agent(
184
+ name=args.agent_name,
185
+ model=LiteLlm(
186
+ model=args.llmmodel,
187
+ #vertex_project=args.llmprojectid,
188
+ vertex_credentials=vertex_credentials,
189
+ vertex_location=args.llmlocation,
190
+ #seed=args.llmseed,
191
+ #temperature=args.llmtemperature,
192
+ ),
193
+ description=description,
194
+ instruction=instruction,
195
+ tools=tools,
196
+ )
197
+ elif args.llmprov == 'ollama':
198
+ if args.llmmodel is None: raise ValueError("llmmodel is required.")
199
+ if args.llmendpoint is None: raise ValueError("llmendpoint is required.")
200
+ agent = Agent(
201
+ name=args.agent_name,
202
+ model=LiteLlm(
203
+ model=f"ollama/{args.llmmodel}",
204
+ api_base=args.llmendpoint,
205
+ temperature=args.llmtemperature,
206
+ stream=True
207
+ ),
208
+ description=description,
209
+ instruction=instruction,
210
+ tools=tools,
211
+ )
212
+ else:
213
+ raise ValueError("llmprov is required.")
214
+ if logger.level == logging.DEBUG:
215
+ logger.debug(f"create_agent complate.")
216
+ return agent
217
+
218
+ def create_runner(self, logger:logging.Logger, args:argparse.Namespace, session_service, agent) -> Any:
219
+ """
220
+ ランナーを作成します
221
+
222
+ Args:
223
+ logger (logging.Logger): ロガー
224
+ args (argparse.Namespace): 引数
225
+ session_service (BaseSessionService): セッションサービス
226
+ agent (Agent): エージェント
227
+
228
+ Returns:
229
+ Runner: ランナー
230
+ """
231
+ from google.adk.runners import Runner
232
+ return Runner(
233
+ app_name=self.ver.__appid__,
234
+ agent=agent,
235
+ session_service=session_service,
236
+ )
237
+
238
+ def init_agent_runner(self, logger:logging.Logger, args:argparse.Namespace) -> Tuple[Any, Any]:
239
+ """
240
+ エージェントの初期化を行います
241
+
242
+ Args:
243
+ logger (logging.Logger): ロガー
244
+ args (argparse.Namespace): 引数
245
+
246
+ Returns:
247
+ Tuple[Any, Any]: ランナーとFastMCP
248
+ """
249
+ if logger.level == logging.DEBUG:
250
+ logger.debug(f"init_agent_runner processing..")
251
+ # loggerの初期化
252
+ common.reset_logger("httpx")
253
+ common.reset_logger("google_adk.google.adk.sessions.database_session_service")
254
+ common.reset_logger("mcp.server.streamable_http_manager")
255
+ # モジュールインポート
256
+ from fastmcp import FastMCP
257
+ from google.adk.sessions import BaseSessionService
258
+ mcp:FastMCP = self.create_mcpserver(args)
259
+ 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)
372
+ runner = self.create_runner(logger, args, session_service, root_agent)
373
+ if logger.level == logging.DEBUG:
374
+ logger.debug(f"init_agent_runner complate.")
375
+ return runner, mcp
cmdbox/app/options.py CHANGED
@@ -295,12 +295,18 @@ class Options:
295
295
  discription_ja="クライアントのメッセージIDを指定します。省略した場合はuuid4で生成されます。",
296
296
  discription_en="Specifies the message ID of the client. If omitted, uuid4 will be generated.",
297
297
  choice=None)
298
+ self._options["description"] = dict(
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.",
302
+ choice=None)
298
303
 
299
304
  def init_debugoption(self):
300
305
  # デバックオプションを追加
301
306
  self._options["debug"]["opt"] = "debug"
302
307
  self._options["tag"]["opt"] = "tag"
303
308
  self._options["clmsg_id"]["opt"] = "clmsg_id"
309
+ self._options["description"]["opt"] = "description"
304
310
  for key, mode in self._options["cmd"].items():
305
311
  if type(mode) is not dict:
306
312
  continue
@@ -315,6 +321,8 @@ class Options:
315
321
  c["choice"].append(self._options["tag"])
316
322
  if "clmsg_id" not in [_o['opt'] for _o in c["choice"]]:
317
323
  c["choice"].append(self._options["clmsg_id"])
324
+ if "description" not in [_o['opt'] for _o in c["choice"]]:
325
+ c["choice"].append(self._options["description"])
318
326
  if c["opt"] not in [_o['opt'] for _o in self._options["cmd"]["choice"]]:
319
327
  self._options["cmd"]["choice"] += [c]
320
328
  self._options["mode"][key] = mode
@@ -824,11 +832,13 @@ class Options:
824
832
  if hasattr(arg, 'redis_password'): opt['password'] = arg.redis_password
825
833
  if hasattr(arg, 'svname'): opt['svname'] = arg.svname
826
834
  if hasattr(arg, 'clmsg_id'): opt['clmsg_id'] = arg.clmsg_id
835
+ if hasattr(arg, 'client_only'): opt['client_only'] = arg.client_only
827
836
  elif isinstance(arg, web.Web):
828
837
  opt['host'] = arg.redis_host
829
838
  opt['port'] = arg.redis_port
830
839
  opt['password'] = arg.redis_password
831
840
  opt['svname'] = arg.svname
841
+ opt['client_only'] = arg.client_only
832
842
  elif isinstance(arg, feature.Feature):
833
843
  func_feature = arg
834
844
  opt['clmsg_src'] = func_feature.__class__.__name__
cmdbox/app/web.py CHANGED
@@ -1,11 +1,9 @@
1
1
  from cmdbox.app import common, options
2
- from cmdbox.app.auth import signin, signin_saml
3
2
  from cmdbox.app.commons import module
4
3
  from fastapi import FastAPI, Request, Response
5
4
  from pathlib import Path
6
5
  from starlette.applications import Starlette
7
6
  from starlette.middleware.sessions import SessionMiddleware
8
- from starlette.routing import Mount
9
7
  from typing import Any, Dict, List
10
8
  from uvicorn.config import Config
11
9
  import asyncio
@@ -13,6 +11,7 @@ import copy
13
11
  import ctypes
14
12
  import datetime
15
13
  import gevent
14
+ import jwt
16
15
  import logging
17
16
  import multiprocessing
18
17
  import os
@@ -20,7 +19,6 @@ import platform
20
19
  import requests
21
20
  import queue
22
21
  import signal
23
- import string
24
22
  import time
25
23
  import threading
26
24
  import traceback
@@ -30,10 +28,10 @@ import webbrowser
30
28
 
31
29
  class Web:
32
30
  def __init__(self, logger:logging.Logger, data:Path, appcls=None, ver=None,
33
- redis_host:str = "localhost", redis_port:int = 6379, redis_password:str = None, svname:str = 'server',
31
+ redis_host:str="localhost", redis_port:int=6379, redis_password:str=None, svname:str='server',
34
32
  client_only:bool=False, doc_root:Path=None, gui_html:str=None, filer_html:str=None, result_html:str=None, users_html:str=None,
35
33
  audit_html:str=None, agent_html:str=None, assets:List[str]=None, signin_html:str=None, signin_file:str=None, gui_mode:bool=False,
36
- web_features_packages:List[str]=None, web_features_prefix:List[str]=None):
34
+ web_features_packages:List[str]=None, web_features_prefix:List[str]=[]):
37
35
  """
38
36
  cmdboxクライアント側のwebapiサービス
39
37
 
@@ -122,7 +120,8 @@ class Web:
122
120
  self.cb_queue = queue.Queue(1000)
123
121
  self.options = options.Options.getInstance()
124
122
  self.webcap_client = requests.Session()
125
- signin_file_data = signin.Signin.load_signin_file(self.signin_file)
123
+ from cmdbox.app.auth import signin, signin_saml
124
+ signin_file_data = signin.Signin.load_signin_file(self.signin_file, self=self)
126
125
  self.signin = signin.Signin(self.logger, self.signin_file, signin_file_data, self.appcls, self.ver)
127
126
  self.signin_saml = signin_saml.SigninSAML(self.logger, self.signin_file, signin_file_data, self.appcls, self.ver)
128
127
 
@@ -249,7 +248,25 @@ class Web:
249
248
  for u in copy.deepcopy(signin_data['users']):
250
249
  u['password'] = '********'
251
250
  if 'apikeys' in u:
252
- u['apikeys'] = dict([(ak, '********') for ak in u['apikeys']])
251
+ for an, ak in u['apikeys'].items():
252
+ exp = '-'
253
+ try:
254
+ cls = self.signin.__class__
255
+ publickey = None
256
+ if cls.verify_jwt_certificate is not None:
257
+ publickey = cls.verify_jwt_certificate.public_key()
258
+ if publickey is None and cls.verify_jwt_publickey is not None:
259
+ publickey = cls.verify_jwt_publickey
260
+ t = jwt.decode(ak, publickey, algorithms=[cls.verify_jwt_algorithm],
261
+ issuer=cls.verify_jwt_issuer, audience=cls.verify_jwt_audience,
262
+ options={'verify_iss': cls.verify_jwt_issuer is not None,
263
+ 'verify_aud': cls.verify_jwt_audience is not None})
264
+ exp = datetime.datetime.fromtimestamp(t['exp']).strftime('%Y-%m-%d %H:%M:%S')
265
+ u['apikeys'][an] = (ak, exp, '-')
266
+ except jwt.exceptions.InvalidTokenError as e:
267
+ u['apikeys'][an] = (ak, '-', str(e))
268
+ except Exception as e:
269
+ u['apikeys'][an] = (ak, '-', '-')
253
270
  if u['name'] == name:
254
271
  return [u]
255
272
  signin_last = self.user_data(None, u['uid'], u['name'], 'signin', 'last_update')
@@ -291,10 +308,19 @@ class Web:
291
308
  if user['apikey_name'] in u['apikeys']:
292
309
  raise ValueError(f"ApiKey name is already exists. ({user})")
293
310
  apikey = common.random_string(64)
294
- u['apikeys'][user['apikey_name']] = common.hash_password(apikey, 'sha1')
311
+ u['apikeys'][user['apikey_name']] = apikey
312
+ if signin_data['apikey']['gen_jwt']['enabled']:
313
+ cls = self.signin.__class__
314
+ claims = cls.gen_jwt_claims.copy() if cls.gen_jwt_claims is not None else dict()
315
+ claims['exp'] = int(time.time()) + int(claims.get('exp', 3600))
316
+ claims['uid'] = u['uid']
317
+ claims['name'] = u['name']
318
+ claims['groups'] = u['groups']
319
+ claims['email'] = u['email']
320
+ claims['apikey_name'] = user['apikey_name']
321
+ apikey = jwt.encode(claims, cls.gen_jwt_privatekey, algorithm=cls.gen_jwt_algorithm)
322
+ u['apikeys'][user['apikey_name']] = apikey
295
323
 
296
- if self.signin_file is None:
297
- raise ValueError(f"signin_file is None.")
298
324
  if self.logger.level == logging.DEBUG:
299
325
  self.logger.debug(f"apikey_add: {user} -> {self.signin_file}")
300
326
  common.save_yml(self.signin_file, signin_data)
@@ -679,7 +705,7 @@ class Web:
679
705
  ssl_cert:Path=None, ssl_key:Path=None, ssl_keypass:str=None, ssl_ca_certs:Path=None,
680
706
  session_domain:str=None, session_path:str='/', session_secure:bool=False, session_timeout:int=900, outputs_key:List[str]=[],
681
707
  guvicorn_workers:int=-1, guvicorn_timeout:int=30,
682
- agent_runner=None, mcp=None, mcp_listen_port=9081, mcp_ssl_listen_port=9443):
708
+ agent_runner=None, mcp=None,):
683
709
  """
684
710
  Webサーバを起動する
685
711
 
@@ -700,8 +726,6 @@ class Web:
700
726
  guvicorn_timeout (int, optional): Gunicornタイムアウト. Defaults to 30.
701
727
  agent_runner (Runner, optional): エージェントランナー. Defaults to None.
702
728
  mcp (MCP, optional): MCP. Defaults to None.
703
- mcp_listen_port (int, optional): MCPリスンポート. Defaults to 9081.
704
- mcp_ssl_listen_port (int, optional): MCP SSLリスンポート. Defaults to 9443.
705
729
  """
706
730
  self.allow_host = allow_host
707
731
  self.listen_port = listen_port
@@ -719,8 +743,6 @@ class Web:
719
743
  self.guvicorn_timeout = guvicorn_timeout
720
744
  self.agent_runner = agent_runner
721
745
  self.mcp = mcp
722
- self.mcp_listen_port = mcp_listen_port
723
- self.mcp_ssl_listen_port = mcp_ssl_listen_port
724
746
  if self.logger.level == logging.DEBUG:
725
747
  self.logger.debug(f"web start parameter: allow_host={self.allow_host}")
726
748
  self.logger.debug(f"web start parameter: listen_port={self.listen_port}")
@@ -738,8 +760,6 @@ class Web:
738
760
  self.logger.debug(f"web start parameter: guvicorn_timeout={self.guvicorn_timeout}")
739
761
  self.logger.debug(f"web start parameter: agent_runner={self.agent_runner}")
740
762
  self.logger.debug(f"web start parameter: mcp={self.mcp}")
741
- self.logger.debug(f"web start parameter: mcp_listen_port={self.mcp_listen_port}")
742
- self.logger.debug(f"web start parameter: mcp_ssl_listen_port={self.mcp_ssl_listen_port}")
743
763
 
744
764
  if self.agent_runner is not None:
745
765
  # google.adkが大きいので必要な時にだけ読込む
@@ -778,7 +798,10 @@ class Web:
778
798
  sessions = await session_service.list_sessions(app_name=self.ver.__appid__, user_id=user_id)
779
799
  ret = []
780
800
  for s in sessions.sessions:
781
- ret.append(await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=s.id))
801
+ session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=s.id)
802
+ if session is None:
803
+ continue
804
+ ret.append(session)
782
805
  return ret
783
806
  else:
784
807
  session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
@@ -801,17 +824,15 @@ class Web:
801
824
  return await session_service.delete_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
802
825
  self.delete_agent_session = delete_agent_session
803
826
 
804
- """
827
+ mcp_app:Starlette = None
805
828
  if self.mcp is not None:
806
- # MCPをFastAPIにマウント
807
829
  mcp_app:Starlette = self.mcp.streamable_http_app()
808
- app = FastAPI(lifespan=self.mcp.settings.lifespan)
809
- app.mount("/mcp", mcp_app)
810
- #app.include_router(mcp_app.router)
830
+ #mcp_app:Starlette = self.mcp.http_app()
831
+ if mcp_app is not None:
832
+ app = FastAPI(lifespan=mcp_app.lifespan)
811
833
  else:
812
834
  app = FastAPI()
813
- """
814
- app = FastAPI()
835
+
815
836
  @app.middleware("http")
816
837
  async def set_context_cookie(req:Request, call_next):
817
838
  res:Response = await call_next(req)
@@ -824,20 +845,13 @@ class Web:
824
845
  if self.session_secure:
825
846
  mwparam['https_only'] = True # セッションハイジャック対策
826
847
  app.add_middleware(SessionMiddleware, **mwparam)
848
+ if mcp_app is not None:
849
+ app.mount("/mcpsv", mcp_app, name="mcp")
850
+ self.logger.info(f"mcp server mount: mount_path=/mcpsv app={mcp_app} routes={mcp_app.routes}")
827
851
  self.init_webfeatures(app)
828
852
 
829
853
  self.is_running = True
830
- #uvicorn.run(app, host=self.allow_host, port=self.listen_port, workers=2)
831
- http_config = Config(app=app, host=self.allow_host, port=self.listen_port)
832
- th = ThreadedUvicorn(self.logger, config=http_config,
833
- guvicorn_config=dict(workers=self.guvicorn_workers, timeout=self.guvicorn_timeout))
834
- th.start()
835
- if self.mcp is not None and self.ssl_cert is None and self.ssl_key is None:
836
- mcp_app:Starlette = self.mcp.streamable_http_app()
837
- http_config = Config(app=mcp_app, host=self.allow_host, port=self.mcp_listen_port)
838
- mcp_th = ThreadedUvicorn(self.logger, config=http_config, force_uvicorn=True)
839
- mcp_th.start()
840
- browser_port = self.listen_port
854
+ th = None
841
855
  th_ssl = None
842
856
  if self.ssl_cert is not None and self.ssl_key is not None:
843
857
  https_config = Config(app=app, host=self.allow_host, port=self.ssl_listen_port,
@@ -847,13 +861,12 @@ class Web:
847
861
  guvicorn_config=dict(workers=self.guvicorn_workers, timeout=self.guvicorn_timeout))
848
862
  th_ssl.start()
849
863
  browser_port = self.ssl_listen_port
850
- if self.mcp is not None:
851
- mcp_app:Starlette = self.mcp.streamable_http_app()
852
- https_config = Config(app=mcp_app, host=self.allow_host, port=self.mcp_ssl_listen_port,
853
- ssl_certfile=self.ssl_cert, ssl_keyfile=self.ssl_key,
854
- ssl_keyfile_password=self.ssl_keypass, ssl_ca_certs=self.ssl_ca_certs)
855
- mcp_th_ssl = ThreadedUvicorn(self.logger, config=https_config, force_uvicorn=True)
856
- mcp_th_ssl.start()
864
+ else:
865
+ http_config = Config(app=app, host=self.allow_host, port=self.listen_port)
866
+ th = ThreadedUvicorn(self.logger, config=http_config,
867
+ guvicorn_config=dict(workers=self.guvicorn_workers, timeout=self.guvicorn_timeout))
868
+ th.start()
869
+ browser_port = self.listen_port
857
870
  try:
858
871
  if self.gui_mode:
859
872
  webbrowser.open(f'http://localhost:{browser_port}/gui')
@@ -861,21 +874,15 @@ class Web:
861
874
  f.write(str(os.getpid()))
862
875
  while self.is_running:
863
876
  gevent.sleep(1)
864
- th.stop()
865
- if self.mcp is not None:
866
- mcp_th.stop()
877
+ if th is not None:
878
+ th.stop()
867
879
  if th_ssl is not None:
868
880
  th_ssl.stop()
869
- if self.mcp is not None:
870
- mcp_th_ssl.stop()
871
881
  except KeyboardInterrupt:
872
- th.stop()
873
- if self.mcp is not None:
874
- mcp_th.stop()
882
+ if th is not None:
883
+ th.stop()
875
884
  if th_ssl is not None:
876
885
  th_ssl.stop()
877
- if self.mcp is not None:
878
- mcp_th_ssl.stop()
879
886
 
880
887
  def stop(self):
881
888
  """