diff --git a/mcpgateway/plugins/framework/base.py b/mcpgateway/plugins/framework/base.py index 1d3e221b9..a329104e9 100644 --- a/mcpgateway/plugins/framework/base.py +++ b/mcpgateway/plugins/framework/base.py @@ -2,7 +2,7 @@ """Location: ./mcpgateway/plugins/framework/base.py Copyright 2025 SPDX-License-Identifier: Apache-2.0 --Authors: Teryl Taylor, Mihai Criveti +Authors: Teryl Taylor, Mihai Criveti Base plugin implementation. This module implements the base plugin object. diff --git a/mcpgateway/plugins/framework/external/mcp/server/server.py b/mcpgateway/plugins/framework/external/mcp/server/server.py index adf8036fe..4f737cc82 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server/server.py @@ -41,7 +41,7 @@ def __init__(self, config_path: str | None = None) -> None: If set, this attribute overrides the value in PLUGINS_CONFIG_PATH. Examples: - >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") + >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway.plugins/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") >>> server is not None True """ @@ -57,7 +57,7 @@ async def get_plugin_configs(self) -> list[dict]: Examples: >>> import asyncio - >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") + >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway.plugins/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") >>> plugins = asyncio.run(server.get_plugin_configs()) >>> len(plugins) > 0 True @@ -79,7 +79,7 @@ async def get_plugin_config(self, name: str) -> dict | None: Examples: >>> import asyncio - >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") + >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway.plugins/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") >>> c = asyncio.run(server.get_plugin_config(name = "DenyListPlugin")) >>> c is not None True diff --git a/mcpgateway/plugins/framework/generated/__init__.py b/mcpgateway/plugins/framework/generated/__init__.py new file mode 100644 index 000000000..ff01e6de1 --- /dev/null +++ b/mcpgateway/plugins/framework/generated/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +"""Generated protobuf Python classes for ContextForge plugins. + +This package contains standard protobuf Python classes (_pb2.py files) generated +from protobuf schemas. These are used for cross-language serialization. + +The canonical Python implementation uses Pydantic models in mcpgateway.plugins.framework.models +which have model_dump_pb() and model_validate_pb() methods for conversion. + +Generated using standard protoc from schemas in protobufs/plugins/schemas/ +""" diff --git a/mcpgateway/plugins/framework/generated/agents_pb2.py b/mcpgateway/plugins/framework/generated/agents_pb2.py new file mode 100644 index 000000000..0e007b568 --- /dev/null +++ b/mcpgateway/plugins/framework/generated/agents_pb2.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcpgateway/plugins/framework/generated/agents.proto +# Protobuf Python Version: 6.33.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'mcpgateway/plugins/framework/generated/agents.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from mcpgateway.plugins.framework.generated import types_pb2 as mcpgateway_dot_plugins_dot_framework_dot_generated_dot_types__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n3mcpgateway/plugins/framework/generated/agents.proto\x12\x1a\x63ontextforge.plugins.hooks\x1a\x1cgoogle/protobuf/struct.proto\x1a\x32mcpgateway/plugins/framework/generated/types.proto\"\xf1\x01\n\x15\x41gentPreInvokePayload\x12\x10\n\x08\x61gent_id\x18\x01 \x01(\t\x12)\n\x08messages\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Struct\x12\r\n\x05tools\x18\x03 \x03(\t\x12\x39\n\x07headers\x18\x04 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\x12\r\n\x05model\x18\x05 \x01(\t\x12\x15\n\rsystem_prompt\x18\x06 \x01(\t\x12+\n\nparameters\x18\x07 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x82\x01\n\x16\x41gentPostInvokePayload\x12\x10\n\x08\x61gent_id\x18\x01 \x01(\t\x12)\n\x08messages\x18\x02 \x03(\x0b\x32\x17.google.protobuf.Struct\x12+\n\ntool_calls\x18\x03 \x03(\x0b\x32\x17.google.protobuf.Struct\"\xc4\x02\n\x14\x41gentPreInvokeResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12K\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x31.contextforge.plugins.hooks.AgentPreInvokePayload\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12P\n\x08metadata\x18\x04 \x03(\x0b\x32>.contextforge.plugins.hooks.AgentPreInvokeResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc7\x02\n\x15\x41gentPostInvokeResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12L\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x32.contextforge.plugins.hooks.AgentPostInvokePayload\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12Q\n\x08metadata\x18\x04 \x03(\x0b\x32?.contextforge.plugins.hooks.AgentPostInvokeResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01*]\n\rAgentHookType\x12\x1f\n\x1b\x41GENT_HOOK_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10\x41GENT_PRE_INVOKE\x10\x01\x12\x15\n\x11\x41GENT_POST_INVOKE\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcpgateway.plugins.framework.generated.agents_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_AGENTPREINVOKERESULT_METADATAENTRY']._loaded_options = None + _globals['_AGENTPREINVOKERESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_AGENTPOSTINVOKERESULT_METADATAENTRY']._loaded_options = None + _globals['_AGENTPOSTINVOKERESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_AGENTHOOKTYPE']._serialized_start=1199 + _globals['_AGENTHOOKTYPE']._serialized_end=1292 + _globals['_AGENTPREINVOKEPAYLOAD']._serialized_start=166 + _globals['_AGENTPREINVOKEPAYLOAD']._serialized_end=407 + _globals['_AGENTPOSTINVOKEPAYLOAD']._serialized_start=410 + _globals['_AGENTPOSTINVOKEPAYLOAD']._serialized_end=540 + _globals['_AGENTPREINVOKERESULT']._serialized_start=543 + _globals['_AGENTPREINVOKERESULT']._serialized_end=867 + _globals['_AGENTPREINVOKERESULT_METADATAENTRY']._serialized_start=820 + _globals['_AGENTPREINVOKERESULT_METADATAENTRY']._serialized_end=867 + _globals['_AGENTPOSTINVOKERESULT']._serialized_start=870 + _globals['_AGENTPOSTINVOKERESULT']._serialized_end=1197 + _globals['_AGENTPOSTINVOKERESULT_METADATAENTRY']._serialized_start=820 + _globals['_AGENTPOSTINVOKERESULT_METADATAENTRY']._serialized_end=867 +# @@protoc_insertion_point(module_scope) diff --git a/mcpgateway/plugins/framework/generated/http_pb2.py b/mcpgateway/plugins/framework/generated/http_pb2.py new file mode 100644 index 000000000..f2f49c064 --- /dev/null +++ b/mcpgateway/plugins/framework/generated/http_pb2.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcpgateway/plugins/framework/generated/http.proto +# Protobuf Python Version: 6.33.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'mcpgateway/plugins/framework/generated/http.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from mcpgateway.plugins.framework.generated import types_pb2 as mcpgateway_dot_plugins_dot_framework_dot_generated_dot_types__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n1mcpgateway/plugins/framework/generated/http.proto\x12\x1a\x63ontextforge.plugins.hooks\x1a\x1cgoogle/protobuf/struct.proto\x1a\x32mcpgateway/plugins/framework/generated/types.proto\"\x9a\x01\n\x15HttpPreRequestPayload\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\x12\x13\n\x0b\x63lient_host\x18\x03 \x01(\t\x12\x13\n\x0b\x63lient_port\x18\x04 \x01(\x05\x12\x39\n\x07headers\x18\x05 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\"\xf4\x01\n\x16HttpPostRequestPayload\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\x12\x13\n\x0b\x63lient_host\x18\x03 \x01(\t\x12\x13\n\x0b\x63lient_port\x18\x04 \x01(\x05\x12\x39\n\x07headers\x18\x05 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\x12\x42\n\x10response_headers\x18\x06 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\x12\x13\n\x0bstatus_code\x18\x07 \x01(\x05\"\xaf\x01\n\x1aHttpAuthResolveUserPayload\x12,\n\x0b\x63redentials\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x39\n\x07headers\x18\x02 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\x12\x13\n\x0b\x63lient_host\x18\x03 \x01(\t\x12\x13\n\x0b\x63lient_port\x18\x04 \x01(\x05\"\xc0\x01\n\x1eHttpAuthCheckPermissionPayload\x12\x12\n\nuser_email\x18\x01 \x01(\t\x12\x12\n\npermission\x18\x02 \x01(\t\x12\x15\n\rresource_type\x18\x03 \x01(\t\x12\x0f\n\x07team_id\x18\x04 \x01(\t\x12\x10\n\x08is_admin\x18\x05 \x01(\x08\x12\x13\n\x0b\x61uth_method\x18\x06 \x01(\t\x12\x13\n\x0b\x63lient_host\x18\x07 \x01(\t\x12\x12\n\nuser_agent\x18\x08 \x01(\t\"G\n$HttpAuthCheckPermissionResultPayload\x12\x0f\n\x07granted\x18\x01 \x01(\x08\x12\x0e\n\x06reason\x18\x02 \x01(\t\"\xbb\x02\n\x14HttpPreRequestResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12\x42\n\x10modified_payload\x18\x02 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12P\n\x08metadata\x18\x04 \x03(\x0b\x32>.contextforge.plugins.hooks.HttpPreRequestResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbd\x02\n\x15HttpPostRequestResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12\x42\n\x10modified_payload\x18\x02 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12Q\n\x08metadata\x18\x04 \x03(\x0b\x32?.contextforge.plugins.hooks.HttpPostRequestResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xb4\x02\n\x19HttpAuthResolveUserResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12\x31\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12U\n\x08metadata\x18\x04 \x03(\x0b\x32\x43.contextforge.plugins.hooks.HttpAuthResolveUserResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xe5\x02\n\x1dHttpAuthCheckPermissionResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12Z\n\x10modified_payload\x18\x02 \x01(\x0b\x32@.contextforge.plugins.hooks.HttpAuthCheckPermissionResultPayload\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12Y\n\x08metadata\x18\x04 \x03(\x0b\x32G.contextforge.plugins.hooks.HttpAuthCheckPermissionResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01*\x97\x01\n\x0cHttpHookType\x12\x1e\n\x1aHTTP_HOOK_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10HTTP_PRE_REQUEST\x10\x01\x12\x15\n\x11HTTP_POST_REQUEST\x10\x02\x12\x1a\n\x16HTTP_AUTH_RESOLVE_USER\x10\x03\x12\x1e\n\x1aHTTP_AUTH_CHECK_PERMISSION\x10\x04\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcpgateway.plugins.framework.generated.http_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_HTTPPREREQUESTRESULT_METADATAENTRY']._loaded_options = None + _globals['_HTTPPREREQUESTRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_HTTPPOSTREQUESTRESULT_METADATAENTRY']._loaded_options = None + _globals['_HTTPPOSTREQUESTRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_HTTPAUTHRESOLVEUSERRESULT_METADATAENTRY']._loaded_options = None + _globals['_HTTPAUTHRESOLVEUSERRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_HTTPAUTHCHECKPERMISSIONRESULT_METADATAENTRY']._loaded_options = None + _globals['_HTTPAUTHCHECKPERMISSIONRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_HTTPHOOKTYPE']._serialized_start=2323 + _globals['_HTTPHOOKTYPE']._serialized_end=2474 + _globals['_HTTPPREREQUESTPAYLOAD']._serialized_start=164 + _globals['_HTTPPREREQUESTPAYLOAD']._serialized_end=318 + _globals['_HTTPPOSTREQUESTPAYLOAD']._serialized_start=321 + _globals['_HTTPPOSTREQUESTPAYLOAD']._serialized_end=565 + _globals['_HTTPAUTHRESOLVEUSERPAYLOAD']._serialized_start=568 + _globals['_HTTPAUTHRESOLVEUSERPAYLOAD']._serialized_end=743 + _globals['_HTTPAUTHCHECKPERMISSIONPAYLOAD']._serialized_start=746 + _globals['_HTTPAUTHCHECKPERMISSIONPAYLOAD']._serialized_end=938 + _globals['_HTTPAUTHCHECKPERMISSIONRESULTPAYLOAD']._serialized_start=940 + _globals['_HTTPAUTHCHECKPERMISSIONRESULTPAYLOAD']._serialized_end=1011 + _globals['_HTTPPREREQUESTRESULT']._serialized_start=1014 + _globals['_HTTPPREREQUESTRESULT']._serialized_end=1329 + _globals['_HTTPPREREQUESTRESULT_METADATAENTRY']._serialized_start=1282 + _globals['_HTTPPREREQUESTRESULT_METADATAENTRY']._serialized_end=1329 + _globals['_HTTPPOSTREQUESTRESULT']._serialized_start=1332 + _globals['_HTTPPOSTREQUESTRESULT']._serialized_end=1649 + _globals['_HTTPPOSTREQUESTRESULT_METADATAENTRY']._serialized_start=1282 + _globals['_HTTPPOSTREQUESTRESULT_METADATAENTRY']._serialized_end=1329 + _globals['_HTTPAUTHRESOLVEUSERRESULT']._serialized_start=1652 + _globals['_HTTPAUTHRESOLVEUSERRESULT']._serialized_end=1960 + _globals['_HTTPAUTHRESOLVEUSERRESULT_METADATAENTRY']._serialized_start=1282 + _globals['_HTTPAUTHRESOLVEUSERRESULT_METADATAENTRY']._serialized_end=1329 + _globals['_HTTPAUTHCHECKPERMISSIONRESULT']._serialized_start=1963 + _globals['_HTTPAUTHCHECKPERMISSIONRESULT']._serialized_end=2320 + _globals['_HTTPAUTHCHECKPERMISSIONRESULT_METADATAENTRY']._serialized_start=1282 + _globals['_HTTPAUTHCHECKPERMISSIONRESULT_METADATAENTRY']._serialized_end=1329 +# @@protoc_insertion_point(module_scope) diff --git a/mcpgateway/plugins/framework/generated/prompts_pb2.py b/mcpgateway/plugins/framework/generated/prompts_pb2.py new file mode 100644 index 000000000..e311b3b6a --- /dev/null +++ b/mcpgateway/plugins/framework/generated/prompts_pb2.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcpgateway/plugins/framework/generated/prompts.proto +# Protobuf Python Version: 6.33.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'mcpgateway/plugins/framework/generated/prompts.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from mcpgateway.plugins.framework.generated import types_pb2 as mcpgateway_dot_plugins_dot_framework_dot_generated_dot_types__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n4mcpgateway/plugins/framework/generated/prompts.proto\x12\x1a\x63ontextforge.plugins.hooks\x1a\x1cgoogle/protobuf/struct.proto\x1a\x32mcpgateway/plugins/framework/generated/types.proto\"\xa2\x01\n\x15PromptPreFetchPayload\x12\x11\n\tprompt_id\x18\x01 \x01(\t\x12I\n\x04\x61rgs\x18\x02 \x03(\x0b\x32;.contextforge.plugins.hooks.PromptPreFetchPayload.ArgsEntry\x1a+\n\tArgsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"T\n\x16PromptPostFetchPayload\x12\x11\n\tprompt_id\x18\x01 \x01(\t\x12\'\n\x06result\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xc4\x02\n\x14PromptPreFetchResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12K\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x31.contextforge.plugins.hooks.PromptPreFetchPayload\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12P\n\x08metadata\x18\x04 \x03(\x0b\x32>.contextforge.plugins.hooks.PromptPreFetchResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc7\x02\n\x15PromptPostFetchResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12L\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x32.contextforge.plugins.hooks.PromptPostFetchPayload\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12Q\n\x08metadata\x18\x04 \x03(\x0b\x32?.contextforge.plugins.hooks.PromptPostFetchResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01*_\n\x0ePromptHookType\x12 \n\x1cPROMPT_HOOK_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10PROMPT_PRE_FETCH\x10\x01\x12\x15\n\x11PROMPT_POST_FETCH\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcpgateway.plugins.framework.generated.prompts_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_PROMPTPREFETCHPAYLOAD_ARGSENTRY']._loaded_options = None + _globals['_PROMPTPREFETCHPAYLOAD_ARGSENTRY']._serialized_options = b'8\001' + _globals['_PROMPTPREFETCHRESULT_METADATAENTRY']._loaded_options = None + _globals['_PROMPTPREFETCHRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_PROMPTPOSTFETCHRESULT_METADATAENTRY']._loaded_options = None + _globals['_PROMPTPOSTFETCHRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_PROMPTHOOKTYPE']._serialized_start=1074 + _globals['_PROMPTHOOKTYPE']._serialized_end=1169 + _globals['_PROMPTPREFETCHPAYLOAD']._serialized_start=167 + _globals['_PROMPTPREFETCHPAYLOAD']._serialized_end=329 + _globals['_PROMPTPREFETCHPAYLOAD_ARGSENTRY']._serialized_start=286 + _globals['_PROMPTPREFETCHPAYLOAD_ARGSENTRY']._serialized_end=329 + _globals['_PROMPTPOSTFETCHPAYLOAD']._serialized_start=331 + _globals['_PROMPTPOSTFETCHPAYLOAD']._serialized_end=415 + _globals['_PROMPTPREFETCHRESULT']._serialized_start=418 + _globals['_PROMPTPREFETCHRESULT']._serialized_end=742 + _globals['_PROMPTPREFETCHRESULT_METADATAENTRY']._serialized_start=695 + _globals['_PROMPTPREFETCHRESULT_METADATAENTRY']._serialized_end=742 + _globals['_PROMPTPOSTFETCHRESULT']._serialized_start=745 + _globals['_PROMPTPOSTFETCHRESULT']._serialized_end=1072 + _globals['_PROMPTPOSTFETCHRESULT_METADATAENTRY']._serialized_start=695 + _globals['_PROMPTPOSTFETCHRESULT_METADATAENTRY']._serialized_end=742 +# @@protoc_insertion_point(module_scope) diff --git a/mcpgateway/plugins/framework/generated/resources_pb2.py b/mcpgateway/plugins/framework/generated/resources_pb2.py new file mode 100644 index 000000000..4040eb2c5 --- /dev/null +++ b/mcpgateway/plugins/framework/generated/resources_pb2.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcpgateway/plugins/framework/generated/resources.proto +# Protobuf Python Version: 6.33.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'mcpgateway/plugins/framework/generated/resources.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from mcpgateway.plugins.framework.generated import types_pb2 as mcpgateway_dot_plugins_dot_framework_dot_generated_dot_types__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6mcpgateway/plugins/framework/generated/resources.proto\x12\x1a\x63ontextforge.plugins.hooks\x1a\x1cgoogle/protobuf/struct.proto\x1a\x32mcpgateway/plugins/framework/generated/types.proto\"Q\n\x17ResourcePreFetchPayload\x12\x0b\n\x03uri\x18\x01 \x01(\t\x12)\n\x08metadata\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"Q\n\x18ResourcePostFetchPayload\x12\x0b\n\x03uri\x18\x01 \x01(\t\x12(\n\x07\x63ontent\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xca\x02\n\x16ResourcePreFetchResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12M\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x33.contextforge.plugins.hooks.ResourcePreFetchPayload\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12R\n\x08metadata\x18\x04 \x03(\x0b\x32@.contextforge.plugins.hooks.ResourcePreFetchResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xcd\x02\n\x17ResourcePostFetchResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12N\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x34.contextforge.plugins.hooks.ResourcePostFetchPayload\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12S\n\x08metadata\x18\x04 \x03(\x0b\x32\x41.contextforge.plugins.hooks.ResourcePostFetchResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01*g\n\x10ResourceHookType\x12\"\n\x1eRESOURCE_HOOK_TYPE_UNSPECIFIED\x10\x00\x12\x16\n\x12RESOURCE_PRE_FETCH\x10\x01\x12\x17\n\x13RESOURCE_POST_FETCH\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcpgateway.plugins.framework.generated.resources_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_RESOURCEPREFETCHRESULT_METADATAENTRY']._loaded_options = None + _globals['_RESOURCEPREFETCHRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_RESOURCEPOSTFETCHRESULT_METADATAENTRY']._loaded_options = None + _globals['_RESOURCEPOSTFETCHRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_RESOURCEHOOKTYPE']._serialized_start=1003 + _globals['_RESOURCEHOOKTYPE']._serialized_end=1106 + _globals['_RESOURCEPREFETCHPAYLOAD']._serialized_start=168 + _globals['_RESOURCEPREFETCHPAYLOAD']._serialized_end=249 + _globals['_RESOURCEPOSTFETCHPAYLOAD']._serialized_start=251 + _globals['_RESOURCEPOSTFETCHPAYLOAD']._serialized_end=332 + _globals['_RESOURCEPREFETCHRESULT']._serialized_start=335 + _globals['_RESOURCEPREFETCHRESULT']._serialized_end=665 + _globals['_RESOURCEPREFETCHRESULT_METADATAENTRY']._serialized_start=618 + _globals['_RESOURCEPREFETCHRESULT_METADATAENTRY']._serialized_end=665 + _globals['_RESOURCEPOSTFETCHRESULT']._serialized_start=668 + _globals['_RESOURCEPOSTFETCHRESULT']._serialized_end=1001 + _globals['_RESOURCEPOSTFETCHRESULT_METADATAENTRY']._serialized_start=618 + _globals['_RESOURCEPOSTFETCHRESULT_METADATAENTRY']._serialized_end=665 +# @@protoc_insertion_point(module_scope) diff --git a/mcpgateway/plugins/framework/generated/tools_pb2.py b/mcpgateway/plugins/framework/generated/tools_pb2.py new file mode 100644 index 000000000..41460b685 --- /dev/null +++ b/mcpgateway/plugins/framework/generated/tools_pb2.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcpgateway/plugins/framework/generated/tools.proto +# Protobuf Python Version: 6.33.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'mcpgateway/plugins/framework/generated/tools.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from mcpgateway.plugins.framework.generated import types_pb2 as mcpgateway_dot_plugins_dot_framework_dot_generated_dot_types__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n2mcpgateway/plugins/framework/generated/tools.proto\x12\x1a\x63ontextforge.plugins.hooks\x1a\x1cgoogle/protobuf/struct.proto\x1a\x32mcpgateway/plugins/framework/generated/types.proto\"\x86\x01\n\x14ToolPreInvokePayload\x12\x0c\n\x04name\x18\x01 \x01(\t\x12%\n\x04\x61rgs\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x39\n\x07headers\x18\x03 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\"N\n\x15ToolPostInvokePayload\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\'\n\x06result\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xc1\x02\n\x13ToolPreInvokeResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12J\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x30.contextforge.plugins.hooks.ToolPreInvokePayload\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12O\n\x08metadata\x18\x04 \x03(\x0b\x32=.contextforge.plugins.hooks.ToolPreInvokeResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x02\n\x14ToolPostInvokeResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12K\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x31.contextforge.plugins.hooks.ToolPostInvokePayload\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12P\n\x08metadata\x18\x04 \x03(\x0b\x32>.contextforge.plugins.hooks.ToolPostInvokeResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01*Y\n\x0cToolHookType\x12\x1e\n\x1aTOOL_HOOK_TYPE_UNSPECIFIED\x10\x00\x12\x13\n\x0fTOOL_PRE_INVOKE\x10\x01\x12\x14\n\x10TOOL_POST_INVOKE\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcpgateway.plugins.framework.generated.tools_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_TOOLPREINVOKERESULT_METADATAENTRY']._loaded_options = None + _globals['_TOOLPREINVOKERESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_TOOLPOSTINVOKERESULT_METADATAENTRY']._loaded_options = None + _globals['_TOOLPOSTINVOKERESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_TOOLHOOKTYPE']._serialized_start=1032 + _globals['_TOOLHOOKTYPE']._serialized_end=1121 + _globals['_TOOLPREINVOKEPAYLOAD']._serialized_start=165 + _globals['_TOOLPREINVOKEPAYLOAD']._serialized_end=299 + _globals['_TOOLPOSTINVOKEPAYLOAD']._serialized_start=301 + _globals['_TOOLPOSTINVOKEPAYLOAD']._serialized_end=379 + _globals['_TOOLPREINVOKERESULT']._serialized_start=382 + _globals['_TOOLPREINVOKERESULT']._serialized_end=703 + _globals['_TOOLPREINVOKERESULT_METADATAENTRY']._serialized_start=656 + _globals['_TOOLPREINVOKERESULT_METADATAENTRY']._serialized_end=703 + _globals['_TOOLPOSTINVOKERESULT']._serialized_start=706 + _globals['_TOOLPOSTINVOKERESULT']._serialized_end=1030 + _globals['_TOOLPOSTINVOKERESULT_METADATAENTRY']._serialized_start=656 + _globals['_TOOLPOSTINVOKERESULT_METADATAENTRY']._serialized_end=703 +# @@protoc_insertion_point(module_scope) diff --git a/mcpgateway/plugins/framework/generated/types_pb2.py b/mcpgateway/plugins/framework/generated/types_pb2.py new file mode 100644 index 000000000..f028ce33d --- /dev/null +++ b/mcpgateway/plugins/framework/generated/types_pb2.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcpgateway/plugins/framework/generated/types.proto +# Protobuf Python Version: 6.33.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 0, + '', + 'mcpgateway/plugins/framework/generated/types.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n2mcpgateway/plugins/framework/generated/types.proto\x12\x1b\x63ontextforge.plugins.common\x1a\x19google/protobuf/any.proto\x1a\x1cgoogle/protobuf/struct.proto\"\xc8\x02\n\rGlobalContext\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0c\n\x04user\x18\x02 \x01(\t\x12\x11\n\ttenant_id\x18\x03 \x01(\t\x12\x11\n\tserver_id\x18\x04 \x01(\t\x12\x44\n\x05state\x18\x05 \x03(\x0b\x32\x35.contextforge.plugins.common.GlobalContext.StateEntry\x12J\n\x08metadata\x18\x06 \x03(\x0b\x32\x38.contextforge.plugins.common.GlobalContext.MetadataEntry\x1a,\n\nStateEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x01\n\x0fPluginViolation\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04\x63ode\x18\x03 \x01(\t\x12(\n\x07\x64\x65tails\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x13\n\x0bplugin_name\x18\x05 \x01(\t\"\xaa\x01\n\x0fPluginCondition\x12\x12\n\nserver_ids\x18\x01 \x03(\t\x12\x12\n\ntenant_ids\x18\x02 \x03(\t\x12\r\n\x05tools\x18\x03 \x03(\t\x12\x0f\n\x07prompts\x18\x04 \x03(\t\x12\x11\n\tresources\x18\x05 \x03(\t\x12\x0e\n\x06\x61gents\x18\x06 \x03(\t\x12\x15\n\ruser_patterns\x18\x07 \x03(\t\x12\x15\n\rcontent_types\x18\x08 \x03(\t\"\x85\x01\n\x0bHttpHeaders\x12\x46\n\x07headers\x18\x01 \x03(\x0b\x32\x35.contextforge.plugins.common.HttpHeaders.HeadersEntry\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc5\x01\n\x18HttpPreForwardingPayload\x12\x13\n\x0btarget_type\x18\x01 \x01(\t\x12\x11\n\ttarget_id\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x0e\n\x06method\x18\x04 \x01(\t\x12\x13\n\x0b\x63lient_host\x18\x05 \x01(\t\x12\x13\n\x0b\x63lient_port\x18\x06 \x01(\x05\x12\x39\n\x07headers\x18\x07 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\"\x9f\x02\n\x19HttpPostForwardingPayload\x12\x13\n\x0btarget_type\x18\x01 \x01(\t\x12\x11\n\ttarget_id\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x0e\n\x06method\x18\x04 \x01(\t\x12\x13\n\x0b\x63lient_host\x18\x05 \x01(\t\x12\x13\n\x0b\x63lient_port\x18\x06 \x01(\x05\x12\x39\n\x07headers\x18\x07 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\x12\x42\n\x10response_headers\x18\x08 \x01(\x0b\x32(.contextforge.plugins.common.HttpHeaders\x12\x13\n\x0bstatus_code\x18\t \x01(\x05\"\x98\x02\n\x0cPluginResult\x12\x1b\n\x13\x63ontinue_processing\x18\x01 \x01(\x08\x12.\n\x10modified_payload\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any\x12?\n\tviolation\x18\x03 \x01(\x0b\x32,.contextforge.plugins.common.PluginViolation\x12I\n\x08metadata\x18\x04 \x03(\x0b\x32\x37.contextforge.plugins.common.PluginResult.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf6\x02\n\rPluginContext\x12\x44\n\x05state\x18\x01 \x03(\x0b\x32\x35.contextforge.plugins.common.PluginContext.StateEntry\x12\x42\n\x0eglobal_context\x18\x02 \x01(\x0b\x32*.contextforge.plugins.common.GlobalContext\x12J\n\x08metadata\x18\x03 \x03(\x0b\x32\x38.contextforge.plugins.common.PluginContext.MetadataEntry\x1a\x45\n\nStateEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct:\x02\x38\x01\x1aH\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct:\x02\x38\x01\"p\n\x10PluginErrorModel\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x13\n\x0bplugin_name\x18\x02 \x01(\t\x12\x0c\n\x04\x63ode\x18\x03 \x01(\t\x12(\n\x07\x64\x65tails\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"k\n\x19MCPTransportTLSConfigBase\x12\x10\n\x08\x63\x65rtfile\x18\x01 \x01(\t\x12\x0f\n\x07keyfile\x18\x02 \x01(\t\x12\x11\n\tca_bundle\x18\x03 \x01(\t\x12\x18\n\x10keyfile_password\x18\x04 \x01(\t\"\x8c\x01\n\x12MCPClientTLSConfig\x12\x10\n\x08\x63\x65rtfile\x18\x01 \x01(\t\x12\x0f\n\x07keyfile\x18\x02 \x01(\t\x12\x11\n\tca_bundle\x18\x03 \x01(\t\x12\x18\n\x10keyfile_password\x18\x04 \x01(\t\x12\x0e\n\x06verify\x18\x05 \x01(\x08\x12\x16\n\x0e\x63heck_hostname\x18\x06 \x01(\x08\"{\n\x12MCPServerTLSConfig\x12\x10\n\x08\x63\x65rtfile\x18\x01 \x01(\t\x12\x0f\n\x07keyfile\x18\x02 \x01(\t\x12\x11\n\tca_bundle\x18\x03 \x01(\t\x12\x18\n\x10keyfile_password\x18\x04 \x01(\t\x12\x15\n\rssl_cert_reqs\x18\x05 \x01(\x05\"k\n\x0fMCPServerConfig\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\x05\x12<\n\x03tls\x18\x03 \x01(\x0b\x32/.contextforge.plugins.common.MCPServerTLSConfig\"\xa7\x01\n\x0fMCPClientConfig\x12\x39\n\x05proto\x18\x01 \x01(\x0e\x32*.contextforge.plugins.common.TransportType\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0e\n\x06script\x18\x03 \x01(\t\x12<\n\x03tls\x18\x04 \x01(\x0b\x32/.contextforge.plugins.common.MCPClientTLSConfig\"L\n\x0c\x42\x61seTemplate\x12\x0f\n\x07\x63ontext\x18\x01 \x03(\t\x12+\n\nextensions\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x7f\n\x0cToolTemplate\x12\x11\n\ttool_name\x18\x01 \x01(\t\x12\x0e\n\x06\x66ields\x18\x02 \x03(\t\x12\x0e\n\x06result\x18\x03 \x01(\x08\x12\x0f\n\x07\x63ontext\x18\x04 \x03(\t\x12+\n\nextensions\x18\x05 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x83\x01\n\x0ePromptTemplate\x12\x13\n\x0bprompt_name\x18\x01 \x01(\t\x12\x0e\n\x06\x66ields\x18\x02 \x03(\t\x12\x0e\n\x06result\x18\x03 \x01(\x08\x12\x0f\n\x07\x63ontext\x18\x04 \x03(\t\x12+\n\nextensions\x18\x05 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x86\x01\n\x10ResourceTemplate\x12\x14\n\x0cresource_uri\x18\x01 \x01(\t\x12\x0e\n\x06\x66ields\x18\x02 \x03(\t\x12\x0e\n\x06result\x18\x03 \x01(\x08\x12\x0f\n\x07\x63ontext\x18\x04 \x03(\t\x12+\n\nextensions\x18\x05 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xc5\x01\n\tAppliedTo\x12\x38\n\x05tools\x18\x01 \x03(\x0b\x32).contextforge.plugins.common.ToolTemplate\x12<\n\x07prompts\x18\x02 \x03(\x0b\x32+.contextforge.plugins.common.PromptTemplate\x12@\n\tresources\x18\x03 \x03(\x0b\x32-.contextforge.plugins.common.ResourceTemplate\"\xbb\x03\n\x0cPluginConfig\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0e\n\x06\x61uthor\x18\x03 \x01(\t\x12\x0c\n\x04kind\x18\x04 \x01(\t\x12\x11\n\tnamespace\x18\x05 \x01(\t\x12\x0f\n\x07version\x18\x06 \x01(\t\x12\r\n\x05hooks\x18\x07 \x03(\t\x12\x0c\n\x04tags\x18\x08 \x03(\t\x12\x35\n\x04mode\x18\t \x01(\x0e\x32\'.contextforge.plugins.common.PluginMode\x12\x10\n\x08priority\x18\n \x01(\x05\x12@\n\nconditions\x18\x0b \x03(\x0b\x32,.contextforge.plugins.common.PluginCondition\x12:\n\napplied_to\x18\x0c \x01(\x0b\x32&.contextforge.plugins.common.AppliedTo\x12\'\n\x06\x63onfig\x18\r \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x39\n\x03mcp\x18\x0e \x01(\x0b\x32,.contextforge.plugins.common.MCPClientConfig\"\x9e\x01\n\x0ePluginManifest\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0e\n\x06\x61uthor\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x0c\n\x04tags\x18\x04 \x03(\t\x12\x17\n\x0f\x61vailable_hooks\x18\x05 \x03(\t\x12/\n\x0e\x64\x65\x66\x61ult_config\x18\x06 \x01(\x0b\x32\x17.google.protobuf.Struct\"\xaf\x01\n\x0ePluginSettings\x12&\n\x1eparallel_execution_within_band\x18\x01 \x01(\x08\x12\x16\n\x0eplugin_timeout\x18\x02 \x01(\x05\x12\x1c\n\x14\x66\x61il_on_plugin_error\x18\x03 \x01(\x08\x12\x19\n\x11\x65nable_plugin_api\x18\x04 \x01(\x08\x12$\n\x1cplugin_health_check_interval\x18\x05 \x01(\x05\"\xe6\x01\n\x06\x43onfig\x12:\n\x07plugins\x18\x01 \x03(\x0b\x32).contextforge.plugins.common.PluginConfig\x12\x13\n\x0bplugin_dirs\x18\x02 \x03(\t\x12\x44\n\x0fplugin_settings\x18\x03 \x01(\x0b\x32+.contextforge.plugins.common.PluginSettings\x12\x45\n\x0fserver_settings\x18\x04 \x01(\x0b\x32,.contextforge.plugins.common.MCPServerConfig*n\n\nPluginMode\x12\x1b\n\x17PLUGIN_MODE_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x45NFORCE\x10\x01\x12\x18\n\x14\x45NFORCE_IGNORE_ERROR\x10\x02\x12\x0e\n\nPERMISSIVE\x10\x03\x12\x0c\n\x08\x44ISABLED\x10\x04*W\n\rTransportType\x12\x1e\n\x1aTRANSPORT_TYPE_UNSPECIFIED\x10\x00\x12\x07\n\x03SSE\x10\x01\x12\t\n\x05STDIO\x10\x02\x12\x12\n\x0eSTREAMABLEHTTP\x10\x03\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcpgateway.plugins.framework.generated.types_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_GLOBALCONTEXT_STATEENTRY']._loaded_options = None + _globals['_GLOBALCONTEXT_STATEENTRY']._serialized_options = b'8\001' + _globals['_GLOBALCONTEXT_METADATAENTRY']._loaded_options = None + _globals['_GLOBALCONTEXT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_HTTPHEADERS_HEADERSENTRY']._loaded_options = None + _globals['_HTTPHEADERS_HEADERSENTRY']._serialized_options = b'8\001' + _globals['_PLUGINRESULT_METADATAENTRY']._loaded_options = None + _globals['_PLUGINRESULT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_PLUGINCONTEXT_STATEENTRY']._loaded_options = None + _globals['_PLUGINCONTEXT_STATEENTRY']._serialized_options = b'8\001' + _globals['_PLUGINCONTEXT_METADATAENTRY']._loaded_options = None + _globals['_PLUGINCONTEXT_METADATAENTRY']._serialized_options = b'8\001' + _globals['_PLUGINMODE']._serialized_start=4530 + _globals['_PLUGINMODE']._serialized_end=4640 + _globals['_TRANSPORTTYPE']._serialized_start=4642 + _globals['_TRANSPORTTYPE']._serialized_end=4729 + _globals['_GLOBALCONTEXT']._serialized_start=141 + _globals['_GLOBALCONTEXT']._serialized_end=469 + _globals['_GLOBALCONTEXT_STATEENTRY']._serialized_start=376 + _globals['_GLOBALCONTEXT_STATEENTRY']._serialized_end=420 + _globals['_GLOBALCONTEXT_METADATAENTRY']._serialized_start=422 + _globals['_GLOBALCONTEXT_METADATAENTRY']._serialized_end=469 + _globals['_PLUGINVIOLATION']._serialized_start=472 + _globals['_PLUGINVIOLATION']._serialized_end=603 + _globals['_PLUGINCONDITION']._serialized_start=606 + _globals['_PLUGINCONDITION']._serialized_end=776 + _globals['_HTTPHEADERS']._serialized_start=779 + _globals['_HTTPHEADERS']._serialized_end=912 + _globals['_HTTPHEADERS_HEADERSENTRY']._serialized_start=866 + _globals['_HTTPHEADERS_HEADERSENTRY']._serialized_end=912 + _globals['_HTTPPREFORWARDINGPAYLOAD']._serialized_start=915 + _globals['_HTTPPREFORWARDINGPAYLOAD']._serialized_end=1112 + _globals['_HTTPPOSTFORWARDINGPAYLOAD']._serialized_start=1115 + _globals['_HTTPPOSTFORWARDINGPAYLOAD']._serialized_end=1402 + _globals['_PLUGINRESULT']._serialized_start=1405 + _globals['_PLUGINRESULT']._serialized_end=1685 + _globals['_PLUGINRESULT_METADATAENTRY']._serialized_start=422 + _globals['_PLUGINRESULT_METADATAENTRY']._serialized_end=469 + _globals['_PLUGINCONTEXT']._serialized_start=1688 + _globals['_PLUGINCONTEXT']._serialized_end=2062 + _globals['_PLUGINCONTEXT_STATEENTRY']._serialized_start=1919 + _globals['_PLUGINCONTEXT_STATEENTRY']._serialized_end=1988 + _globals['_PLUGINCONTEXT_METADATAENTRY']._serialized_start=1990 + _globals['_PLUGINCONTEXT_METADATAENTRY']._serialized_end=2062 + _globals['_PLUGINERRORMODEL']._serialized_start=2064 + _globals['_PLUGINERRORMODEL']._serialized_end=2176 + _globals['_MCPTRANSPORTTLSCONFIGBASE']._serialized_start=2178 + _globals['_MCPTRANSPORTTLSCONFIGBASE']._serialized_end=2285 + _globals['_MCPCLIENTTLSCONFIG']._serialized_start=2288 + _globals['_MCPCLIENTTLSCONFIG']._serialized_end=2428 + _globals['_MCPSERVERTLSCONFIG']._serialized_start=2430 + _globals['_MCPSERVERTLSCONFIG']._serialized_end=2553 + _globals['_MCPSERVERCONFIG']._serialized_start=2555 + _globals['_MCPSERVERCONFIG']._serialized_end=2662 + _globals['_MCPCLIENTCONFIG']._serialized_start=2665 + _globals['_MCPCLIENTCONFIG']._serialized_end=2832 + _globals['_BASETEMPLATE']._serialized_start=2834 + _globals['_BASETEMPLATE']._serialized_end=2910 + _globals['_TOOLTEMPLATE']._serialized_start=2912 + _globals['_TOOLTEMPLATE']._serialized_end=3039 + _globals['_PROMPTTEMPLATE']._serialized_start=3042 + _globals['_PROMPTTEMPLATE']._serialized_end=3173 + _globals['_RESOURCETEMPLATE']._serialized_start=3176 + _globals['_RESOURCETEMPLATE']._serialized_end=3310 + _globals['_APPLIEDTO']._serialized_start=3313 + _globals['_APPLIEDTO']._serialized_end=3510 + _globals['_PLUGINCONFIG']._serialized_start=3513 + _globals['_PLUGINCONFIG']._serialized_end=3956 + _globals['_PLUGINMANIFEST']._serialized_start=3959 + _globals['_PLUGINMANIFEST']._serialized_end=4117 + _globals['_PLUGINSETTINGS']._serialized_start=4120 + _globals['_PLUGINSETTINGS']._serialized_end=4295 + _globals['_CONFIG']._serialized_start=4298 + _globals['_CONFIG']._serialized_end=4528 +# @@protoc_insertion_point(module_scope) diff --git a/mcpgateway/plugins/framework/hooks/agents.py b/mcpgateway/plugins/framework/hooks/agents.py index eea547c9a..12461d10a 100644 --- a/mcpgateway/plugins/framework/hooks/agents.py +++ b/mcpgateway/plugins/framework/hooks/agents.py @@ -86,6 +86,93 @@ class AgentPreInvokePayload(PluginPayload): system_prompt: Optional[str] = None parameters: Optional[Dict[str, Any]] = Field(default_factory=dict) + def model_dump_pb(self): + """Convert to protobuf AgentPreInvokePayload message. + + Returns: + agents_pb2.AgentPreInvokePayload: Protobuf message. + """ + # Third-Party + from google.protobuf import json_format, struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import agents_pb2 + + # Convert messages list to repeated Struct + messages_pb = [] + for msg in self.messages: + msg_struct = struct_pb2.Struct() + msg_dict = msg.model_dump(mode="json") + json_format.ParseDict(msg_dict, msg_struct) + messages_pb.append(msg_struct) + + # Convert parameters dict to Struct + parameters_struct = struct_pb2.Struct() + if self.parameters: + json_format.ParseDict(self.parameters, parameters_struct) + + # Convert headers if present + headers_pb = None + if self.headers: + # First-Party + from mcpgateway.plugins.framework.generated import types_pb2 + + # HttpHeaderPayload is a RootModel, extract the root dict + headers_dict = self.headers.root if hasattr(self.headers, "root") else self.headers + headers_pb = types_pb2.HttpHeaders(headers=headers_dict) + + return agents_pb2.AgentPreInvokePayload( + agent_id=self.agent_id, + messages=messages_pb, + tools=self.tools or [], + headers=headers_pb, + model=self.model or "", + system_prompt=self.system_prompt or "", + parameters=parameters_struct, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "AgentPreInvokePayload": + """Create from protobuf AgentPreInvokePayload message. + + Args: + proto: agents_pb2.AgentPreInvokePayload protobuf message. + + Returns: + AgentPreInvokePayload: Pydantic model instance. + """ + # Third-Party + from google.protobuf import json_format + + # Convert repeated Struct to list of Message + messages = [] + for msg_struct in proto.messages: + msg_dict = json_format.MessageToDict(msg_struct) + messages.append(Message.model_validate(msg_dict)) + + # Convert Struct to dict + parameters = {} + if proto.HasField("parameters"): + parameters = json_format.MessageToDict(proto.parameters) + + # Convert headers if present + headers = None + if proto.HasField("headers"): + # First-Party + from mcpgateway.plugins.framework.hooks.http import HttpHeaderPayload + + headers = HttpHeaderPayload(dict(proto.headers.headers)) + + return cls( + agent_id=proto.agent_id, + messages=messages, + tools=list(proto.tools) if proto.tools else None, + headers=headers, + model=proto.model if proto.model else None, + system_prompt=proto.system_prompt if proto.system_prompt else None, + parameters=parameters, + ) + class AgentPostInvokePayload(PluginPayload): """Agent payload for post-invoke hook. @@ -118,6 +205,73 @@ class AgentPostInvokePayload(PluginPayload): messages: List[Message] tool_calls: Optional[List[Dict[str, Any]]] = None + def model_dump_pb(self): + """Convert to protobuf AgentPostInvokePayload message. + + Returns: + agents_pb2.AgentPostInvokePayload: Protobuf message. + """ + # Third-Party + from google.protobuf import json_format, struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import agents_pb2 + + # Convert messages list to repeated Struct + messages_pb = [] + for msg in self.messages: + msg_struct = struct_pb2.Struct() + msg_dict = msg.model_dump(mode="json") + json_format.ParseDict(msg_dict, msg_struct) + messages_pb.append(msg_struct) + + # Convert tool_calls list to repeated Struct + tool_calls_pb = [] + if self.tool_calls: + for tool_call in self.tool_calls: + tool_call_struct = struct_pb2.Struct() + json_format.ParseDict(tool_call, tool_call_struct) + tool_calls_pb.append(tool_call_struct) + + return agents_pb2.AgentPostInvokePayload( + agent_id=self.agent_id, + messages=messages_pb, + tool_calls=tool_calls_pb, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "AgentPostInvokePayload": + """Create from protobuf AgentPostInvokePayload message. + + Args: + proto: agents_pb2.AgentPostInvokePayload protobuf message. + + Returns: + AgentPostInvokePayload: Pydantic model instance. + """ + # Third-Party + from google.protobuf import json_format + + # Convert repeated Struct to list of Message + messages = [] + for msg_struct in proto.messages: + msg_dict = json_format.MessageToDict(msg_struct) + messages.append(Message.model_validate(msg_dict)) + + # Convert repeated Struct to list of tool calls + tool_calls = None + if proto.tool_calls: + tool_calls = [] + for tool_call_struct in proto.tool_calls: + tool_call_dict = json_format.MessageToDict(tool_call_struct) + tool_calls.append(tool_call_dict) + + return cls( + agent_id=proto.agent_id, + messages=messages, + tool_calls=tool_calls, + ) + AgentPreInvokeResult = PluginResult[AgentPreInvokePayload] AgentPostInvokeResult = PluginResult[AgentPostInvokePayload] diff --git a/mcpgateway/plugins/framework/hooks/http.py b/mcpgateway/plugins/framework/hooks/http.py index 163091097..7b9346c96 100644 --- a/mcpgateway/plugins/framework/hooks/http.py +++ b/mcpgateway/plugins/framework/hooks/http.py @@ -97,6 +97,47 @@ class HttpPreRequestPayload(PluginPayload): client_port: int | None = None headers: HttpHeaderPayload + def model_dump_pb(self): + """Convert to protobuf HttpPreRequestPayload message. + + Returns: + http_pb2.HttpPreRequestPayload: Protobuf message. + """ + # First-Party + from mcpgateway.plugins.framework.generated import http_pb2, types_pb2 + + # Convert headers + headers_dict = self.headers.root if hasattr(self.headers, "root") else self.headers + headers_pb = types_pb2.HttpHeaders(headers=headers_dict) + + return http_pb2.HttpPreRequestPayload( + path=self.path, + method=self.method, + client_host=self.client_host or "", + client_port=self.client_port or 0, + headers=headers_pb, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "HttpPreRequestPayload": + """Create from protobuf HttpPreRequestPayload message. + + Args: + proto: http_pb2.HttpPreRequestPayload protobuf message. + + Returns: + HttpPreRequestPayload: Pydantic model instance. + """ + headers = HttpHeaderPayload(dict(proto.headers.headers)) if proto.HasField("headers") else HttpHeaderPayload({}) + + return cls( + path=proto.path, + method=proto.method, + client_host=proto.client_host if proto.client_host else None, + client_port=proto.client_port if proto.client_port else None, + headers=headers, + ) + class HttpPostRequestPayload(HttpPreRequestPayload): """Payload for HTTP post-request hook (middleware layer). @@ -113,6 +154,58 @@ class HttpPostRequestPayload(HttpPreRequestPayload): response_headers: HttpHeaderPayload | None = None status_code: int | None = None + def model_dump_pb(self): + """Convert to protobuf HttpPostRequestPayload message. + + Returns: + http_pb2.HttpPostRequestPayload: Protobuf message. + """ + # First-Party + from mcpgateway.plugins.framework.generated import http_pb2, types_pb2 + + # Convert headers + headers_dict = self.headers.root if hasattr(self.headers, "root") else self.headers + headers_pb = types_pb2.HttpHeaders(headers=headers_dict) + + # Convert response headers if present + response_headers_pb = None + if self.response_headers: + response_dict = self.response_headers.root if hasattr(self.response_headers, "root") else self.response_headers + response_headers_pb = types_pb2.HttpHeaders(headers=response_dict) + + return http_pb2.HttpPostRequestPayload( + path=self.path, + method=self.method, + client_host=self.client_host or "", + client_port=self.client_port or 0, + headers=headers_pb, + response_headers=response_headers_pb, + status_code=self.status_code or 0, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "HttpPostRequestPayload": + """Create from protobuf HttpPostRequestPayload message. + + Args: + proto: http_pb2.HttpPostRequestPayload protobuf message. + + Returns: + HttpPostRequestPayload: Pydantic model instance. + """ + headers = HttpHeaderPayload(dict(proto.headers.headers)) if proto.HasField("headers") else HttpHeaderPayload({}) + response_headers = HttpHeaderPayload(dict(proto.response_headers.headers)) if proto.HasField("response_headers") else None + + return cls( + path=proto.path, + method=proto.method, + client_host=proto.client_host if proto.client_host else None, + client_port=proto.client_port if proto.client_port else None, + headers=headers, + response_headers=response_headers, + status_code=proto.status_code if proto.status_code else None, + ) + class HttpAuthResolveUserPayload(PluginPayload): """Payload for custom user authentication hook (auth layer). @@ -133,6 +226,62 @@ class HttpAuthResolveUserPayload(PluginPayload): client_host: str | None = None client_port: int | None = None + def model_dump_pb(self): + """Convert to protobuf HttpAuthResolveUserPayload message. + + Returns: + http_pb2.HttpAuthResolveUserPayload: Protobuf message. + """ + # Third-Party + from google.protobuf import json_format, struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import http_pb2, types_pb2 + + # Convert credentials dict to Struct + credentials_struct = None + if self.credentials: + credentials_struct = struct_pb2.Struct() + json_format.ParseDict(self.credentials, credentials_struct) + + # Convert headers + headers_dict = self.headers.root if hasattr(self.headers, "root") else self.headers + headers_pb = types_pb2.HttpHeaders(headers=headers_dict) + + return http_pb2.HttpAuthResolveUserPayload( + credentials=credentials_struct, + headers=headers_pb, + client_host=self.client_host or "", + client_port=self.client_port or 0, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "HttpAuthResolveUserPayload": + """Create from protobuf HttpAuthResolveUserPayload message. + + Args: + proto: http_pb2.HttpAuthResolveUserPayload protobuf message. + + Returns: + HttpAuthResolveUserPayload: Pydantic model instance. + """ + # Third-Party + from google.protobuf import json_format + + # Convert Struct to dict + credentials = None + if proto.HasField("credentials"): + credentials = json_format.MessageToDict(proto.credentials) + + headers = HttpHeaderPayload(dict(proto.headers.headers)) if proto.HasField("headers") else HttpHeaderPayload({}) + + return cls( + credentials=credentials, + headers=headers, + client_host=proto.client_host if proto.client_host else None, + client_port=proto.client_port if proto.client_port else None, + ) + class HttpAuthCheckPermissionPayload(PluginPayload): """Payload for permission checking hook (RBAC layer). @@ -163,6 +312,47 @@ class HttpAuthCheckPermissionPayload(PluginPayload): client_host: str | None = None user_agent: str | None = None + def model_dump_pb(self): + """Convert to protobuf HttpAuthCheckPermissionPayload message. + + Returns: + http_pb2.HttpAuthCheckPermissionPayload: Protobuf message. + """ + # First-Party + from mcpgateway.plugins.framework.generated import http_pb2 + + return http_pb2.HttpAuthCheckPermissionPayload( + user_email=self.user_email, + permission=self.permission, + resource_type=self.resource_type or "", + team_id=self.team_id or "", + is_admin=self.is_admin, + auth_method=self.auth_method or "", + client_host=self.client_host or "", + user_agent=self.user_agent or "", + ) + + @classmethod + def model_validate_pb(cls, proto) -> "HttpAuthCheckPermissionPayload": + """Create from protobuf HttpAuthCheckPermissionPayload message. + + Args: + proto: http_pb2.HttpAuthCheckPermissionPayload protobuf message. + + Returns: + HttpAuthCheckPermissionPayload: Pydantic model instance. + """ + return cls( + user_email=proto.user_email, + permission=proto.permission, + resource_type=proto.resource_type if proto.resource_type else None, + team_id=proto.team_id if proto.team_id else None, + is_admin=proto.is_admin, + auth_method=proto.auth_method if proto.auth_method else None, + client_host=proto.client_host if proto.client_host else None, + user_agent=proto.user_agent if proto.user_agent else None, + ) + class HttpAuthCheckPermissionResultPayload(PluginPayload): """Result payload for permission checking hook. @@ -177,6 +367,35 @@ class HttpAuthCheckPermissionResultPayload(PluginPayload): granted: bool reason: str | None = None + def model_dump_pb(self): + """Convert to protobuf HttpAuthCheckPermissionResultPayload message. + + Returns: + http_pb2.HttpAuthCheckPermissionResultPayload: Protobuf message. + """ + # First-Party + from mcpgateway.plugins.framework.generated import http_pb2 + + return http_pb2.HttpAuthCheckPermissionResultPayload( + granted=self.granted, + reason=self.reason or "", + ) + + @classmethod + def model_validate_pb(cls, proto) -> "HttpAuthCheckPermissionResultPayload": + """Create from protobuf HttpAuthCheckPermissionResultPayload message. + + Args: + proto: http_pb2.HttpAuthCheckPermissionResultPayload protobuf message. + + Returns: + HttpAuthCheckPermissionResultPayload: Pydantic model instance. + """ + return cls( + granted=proto.granted, + reason=proto.reason if proto.reason else None, + ) + # Type aliases for hook results HttpPreRequestResult = PluginResult[HttpHeaderPayload] diff --git a/mcpgateway/plugins/framework/hooks/prompts.py b/mcpgateway/plugins/framework/hooks/prompts.py index d57e6bf34..85855bbf4 100644 --- a/mcpgateway/plugins/framework/hooks/prompts.py +++ b/mcpgateway/plugins/framework/hooks/prompts.py @@ -73,6 +73,35 @@ class PromptPrehookPayload(PluginPayload): prompt_id: str args: Optional[dict[str, str]] = Field(default_factory=dict) + def model_dump_pb(self): + """Convert to protobuf PromptPreFetchPayload message. + + Returns: + prompts_pb2.PromptPreFetchPayload: Protobuf message. + """ + # First-Party + from mcpgateway.plugins.framework.generated import prompts_pb2 + + return prompts_pb2.PromptPreFetchPayload( + prompt_id=self.prompt_id, + args=self.args or {}, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "PromptPrehookPayload": + """Create from protobuf PromptPreFetchPayload message. + + Args: + proto: prompts_pb2.PromptPreFetchPayload protobuf message. + + Returns: + PromptPrehookPayload: Pydantic model instance. + """ + return cls( + prompt_id=proto.prompt_id, + args=dict(proto.args) if proto.args else {}, + ) + class PromptPosthookPayload(PluginPayload): """A prompt payload for a prompt posthook. @@ -101,6 +130,56 @@ class PromptPosthookPayload(PluginPayload): prompt_id: str result: PromptResult + def model_dump_pb(self): + """Convert to protobuf PromptPostFetchPayload message. + + Returns: + prompts_pb2.PromptPostFetchPayload: Protobuf message. + """ + # Third-Party + from google.protobuf import json_format, struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import prompts_pb2 + + # Convert PromptResult to Struct + result_struct = struct_pb2.Struct() + if self.result is not None: + # Use Pydantic's model_dump to get dict representation + result_dict = self.result.model_dump(mode="json") + json_format.ParseDict(result_dict, result_struct) + + return prompts_pb2.PromptPostFetchPayload( + prompt_id=self.prompt_id, + result=result_struct, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "PromptPosthookPayload": + """Create from protobuf PromptPostFetchPayload message. + + Args: + proto: prompts_pb2.PromptPostFetchPayload protobuf message. + + Returns: + PromptPosthookPayload: Pydantic model instance. + """ + # Third-Party + from google.protobuf import json_format + + # Convert Struct back to dict + result_dict = None + if proto.HasField("result"): + result_dict = json_format.MessageToDict(proto.result) + + # Reconstruct PromptResult from dict + result = PromptResult.model_validate(result_dict) if result_dict else None + + return cls( + prompt_id=proto.prompt_id, + result=result, + ) + PromptPrehookResult = PluginResult[PromptPrehookPayload] PromptPosthookResult = PluginResult[PromptPosthookPayload] diff --git a/mcpgateway/plugins/framework/hooks/resources.py b/mcpgateway/plugins/framework/hooks/resources.py index b31439130..e7a69c083 100644 --- a/mcpgateway/plugins/framework/hooks/resources.py +++ b/mcpgateway/plugins/framework/hooks/resources.py @@ -64,6 +64,51 @@ class ResourcePreFetchPayload(PluginPayload): uri: str metadata: Optional[dict[str, Any]] = Field(default_factory=dict) + def model_dump_pb(self): + """Convert to protobuf ResourcePreFetchPayload message. + + Returns: + resources_pb2.ResourcePreFetchPayload: Protobuf message. + """ + # Third-Party + from google.protobuf import json_format, struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import resources_pb2 + + # Convert metadata dict to Struct + metadata_struct = struct_pb2.Struct() + if self.metadata: + json_format.ParseDict(self.metadata, metadata_struct) + + return resources_pb2.ResourcePreFetchPayload( + uri=self.uri, + metadata=metadata_struct, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "ResourcePreFetchPayload": + """Create from protobuf ResourcePreFetchPayload message. + + Args: + proto: resources_pb2.ResourcePreFetchPayload protobuf message. + + Returns: + ResourcePreFetchPayload: Pydantic model instance. + """ + # Third-Party + from google.protobuf import json_format + + # Convert Struct to dict + metadata = {} + if proto.HasField("metadata"): + metadata = json_format.MessageToDict(proto.metadata) + + return cls( + uri=proto.uri, + metadata=metadata, + ) + class ResourcePostFetchPayload(PluginPayload): """A resource payload for a resource post-fetch hook. @@ -91,6 +136,64 @@ class ResourcePostFetchPayload(PluginPayload): uri: str content: Any + def model_dump_pb(self): + """Convert to protobuf ResourcePostFetchPayload message. + + Returns: + resources_pb2.ResourcePostFetchPayload: Protobuf message. + """ + # Third-Party + from google.protobuf import json_format, struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import resources_pb2 + + # Convert content to Struct + content_struct = struct_pb2.Struct() + if self.content is not None: + if isinstance(self.content, dict): + json_format.ParseDict(self.content, content_struct) + elif hasattr(self.content, "model_dump"): + # Handle Pydantic models like ResourceContent + content_dict = self.content.model_dump(mode="json") + json_format.ParseDict(content_dict, content_struct) + else: + # For other types, wrap in a dict + json_format.ParseDict({"value": self.content}, content_struct) + + return resources_pb2.ResourcePostFetchPayload( + uri=self.uri, + content=content_struct, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "ResourcePostFetchPayload": + """Create from protobuf ResourcePostFetchPayload message. + + Args: + proto: resources_pb2.ResourcePostFetchPayload protobuf message. + + Returns: + ResourcePostFetchPayload: Pydantic model instance. + """ + # Third-Party + from google.protobuf import json_format + + # Convert Struct to dict/value + content = None + if proto.HasField("content"): + content_dict = json_format.MessageToDict(proto.content) + # If it was wrapped with "value" key, unwrap it + if len(content_dict) == 1 and "value" in content_dict: + content = content_dict["value"] + else: + content = content_dict + + return cls( + uri=proto.uri, + content=content, + ) + ResourcePreFetchResult = PluginResult[ResourcePreFetchPayload] ResourcePostFetchResult = PluginResult[ResourcePostFetchPayload] diff --git a/mcpgateway/plugins/framework/hooks/tools.py b/mcpgateway/plugins/framework/hooks/tools.py index 7560d05b0..a799ec205 100644 --- a/mcpgateway/plugins/framework/hooks/tools.py +++ b/mcpgateway/plugins/framework/hooks/tools.py @@ -70,6 +70,71 @@ class ToolPreInvokePayload(PluginPayload): args: Optional[dict[str, Any]] = Field(default_factory=dict) headers: Optional[HttpHeaderPayload] = None + def model_dump_pb(self): + """Convert to protobuf ToolPreInvokePayload message. + + Returns: + tools_pb2.ToolPreInvokePayload: Protobuf message. + """ + # Third-Party + from google.protobuf import json_format, struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import tools_pb2 + + # Convert args dict to Struct + args_struct = struct_pb2.Struct() + if self.args: + json_format.ParseDict(self.args, args_struct) + + # Convert headers if present + headers_pb = None + if self.headers: + # First-Party + from mcpgateway.plugins.framework.generated import types_pb2 + + # HttpHeaderPayload is a RootModel, extract the root dict + headers_dict = self.headers.root if hasattr(self.headers, "root") else self.headers + headers_pb = types_pb2.HttpHeaders(headers=headers_dict) + + return tools_pb2.ToolPreInvokePayload( + name=self.name, + args=args_struct, + headers=headers_pb, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "ToolPreInvokePayload": + """Create from protobuf ToolPreInvokePayload message. + + Args: + proto: tools_pb2.ToolPreInvokePayload protobuf message. + + Returns: + ToolPreInvokePayload: Pydantic model instance. + """ + # Third-Party + from google.protobuf import json_format + + # Convert Struct to dict + args = {} + if proto.HasField("args"): + args = json_format.MessageToDict(proto.args) + + # Convert headers if present + headers = None + if proto.HasField("headers"): + # First-Party + from mcpgateway.plugins.framework.hooks.http import HttpHeaderPayload + + headers = HttpHeaderPayload(dict(proto.headers.headers)) + + return cls( + name=proto.name, + args=args, + headers=headers, + ) + class ToolPostInvokePayload(PluginPayload): """A tool payload for a tool post-invoke hook. @@ -94,6 +159,60 @@ class ToolPostInvokePayload(PluginPayload): name: str result: Any + def model_dump_pb(self): + """Convert to protobuf ToolPostInvokePayload message. + + Returns: + tools_pb2.ToolPostInvokePayload: Protobuf message. + """ + # Third-Party + from google.protobuf import json_format, struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import tools_pb2 + + # Convert result to Struct + result_struct = struct_pb2.Struct() + if self.result is not None: + if isinstance(self.result, dict): + json_format.ParseDict(self.result, result_struct) + else: + # For non-dict results, wrap in a dict + json_format.ParseDict({"value": self.result}, result_struct) + + return tools_pb2.ToolPostInvokePayload( + name=self.name, + result=result_struct, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "ToolPostInvokePayload": + """Create from protobuf ToolPostInvokePayload message. + + Args: + proto: tools_pb2.ToolPostInvokePayload protobuf message. + + Returns: + ToolPostInvokePayload: Pydantic model instance. + """ + # Third-Party + from google.protobuf import json_format + + # Convert Struct to dict/value + result = None + if proto.HasField("result"): + result_dict = json_format.MessageToDict(proto.result) + # If it was wrapped with "value" key, unwrap it + if len(result_dict) == 1 and "value" in result_dict: + result = result_dict["value"] + else: + result = result_dict + + return cls( + name=proto.name, + result=result, + ) + ToolPreInvokeResult = PluginResult[ToolPreInvokePayload] ToolPostInvokeResult = PluginResult[ToolPostInvokePayload] diff --git a/mcpgateway/plugins/framework/models.py b/mcpgateway/plugins/framework/models.py index d6644abc3..1d901752b 100644 --- a/mcpgateway/plugins/framework/models.py +++ b/mcpgateway/plugins/framework/models.py @@ -719,6 +719,71 @@ def plugin_name(self, name: str) -> None: raise ValueError("Name must be a non-empty string.") self._plugin_name = name + def model_dump_pb(self): + """Convert to protobuf PluginViolation message. + + Returns: + types_pb2.PluginViolation: Protobuf message. + + Note: + Lazy imports protobuf to avoid dependency if not needed. + """ + # Third-Party + from google.protobuf import struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import types_pb2 + + # Convert details dict to Struct + details_struct = struct_pb2.Struct() + if self.details: + for key, value in self.details.items(): + if isinstance(value, dict): + details_struct[key] = value + elif isinstance(value, (list, tuple)): + details_struct[key] = list(value) + else: + details_struct[key] = value + + return types_pb2.PluginViolation( + reason=self.reason, + description=self.description, + code=self.code, + details=details_struct if self.details else None, + plugin_name=self._plugin_name, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "PluginViolation": + """Create from protobuf PluginViolation message. + + Args: + proto: types_pb2.PluginViolation protobuf message. + + Returns: + PluginViolation: Pydantic model instance. + + Note: + Lazy imports protobuf to avoid dependency if not needed. + """ + # Third-Party + from google.protobuf import json_format + + # Convert Struct to dict + details = {} + if proto.HasField("details"): + details = json_format.MessageToDict(proto.details) + + violation = cls( + reason=proto.reason, + description=proto.description, + code=proto.code, + details=details, + ) + if proto.plugin_name: + violation._plugin_name = proto.plugin_name + return violation + class PluginSettings(BaseModel): """Global plugin settings. @@ -791,6 +856,67 @@ class PluginResult(BaseModel, Generic[T]): violation: Optional[PluginViolation] = None metadata: Optional[dict[str, Any]] = Field(default_factory=dict) + def model_dump_pb(self): + """Convert to protobuf PluginResult message. + + Returns: + types_pb2.PluginResult: Protobuf message. + + Note: + Lazy imports protobuf to avoid dependency if not needed. + The modified_payload will be serialized using google.protobuf.Any. + """ + # Third-Party + from google.protobuf import any_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import types_pb2 + + # Handle modified_payload - need to convert to Any if present + modified_payload_any = None + if self.modified_payload is not None: + # If modified_payload has model_dump_pb, use it + if hasattr(self.modified_payload, "model_dump_pb"): + payload_pb = self.modified_payload.model_dump_pb() + modified_payload_any = any_pb2.Any() + modified_payload_any.Pack(payload_pb) + + return types_pb2.PluginResult( + continue_processing=self.continue_processing, + modified_payload=modified_payload_any, + violation=self.violation.model_dump_pb() if self.violation else None, + metadata=self.metadata or {}, + ) + + @classmethod + def model_validate_pb(cls, proto, payload_type=None) -> "PluginResult": + """Create from protobuf PluginResult message. + + Args: + proto: types_pb2.PluginResult protobuf message. + payload_type: Optional Pydantic class to deserialize modified_payload. + + Returns: + PluginResult: Pydantic model instance. + + Note: + Lazy imports protobuf to avoid dependency if not needed. + If payload_type is provided and has model_validate_pb, it will be used. + """ + modified_payload = None + if proto.HasField("modified_payload") and payload_type: + # Try to unpack and convert if payload_type has model_validate_pb + if hasattr(payload_type, "model_validate_pb"): + # This requires knowing the protobuf type - left as future enhancement + pass + + return cls( + continue_processing=proto.continue_processing, + modified_payload=modified_payload, + violation=PluginViolation.model_validate_pb(proto.violation) if proto.HasField("violation") else None, + metadata=dict(proto.metadata), + ) + class GlobalContext(BaseModel): """The global context, which shared across all plugins. @@ -828,6 +954,49 @@ class GlobalContext(BaseModel): state: dict[str, Any] = Field(default_factory=dict) metadata: dict[str, Any] = Field(default_factory=dict) + def model_dump_pb(self): + """Convert to protobuf GlobalContext message. + + Returns: + types_pb2.GlobalContext: Protobuf message. + + Note: + Lazy imports protobuf to avoid dependency if not needed. + """ + # First-Party + from mcpgateway.plugins.framework.generated import types_pb2 + + return types_pb2.GlobalContext( + request_id=self.request_id, + user=self.user or "", + tenant_id=self.tenant_id or "", + server_id=self.server_id or "", + state=self.state, + metadata={k: str(v) for k, v in self.metadata.items()}, # proto expects string values + ) + + @classmethod + def model_validate_pb(cls, proto) -> "GlobalContext": + """Create from protobuf GlobalContext message. + + Args: + proto: types_pb2.GlobalContext protobuf message. + + Returns: + GlobalContext: Pydantic model instance. + + Note: + Lazy imports protobuf to avoid dependency if not needed. + """ + return cls( + request_id=proto.request_id, + user=proto.user if proto.user else None, + tenant_id=proto.tenant_id if proto.tenant_id else None, + server_id=proto.server_id if proto.server_id else None, + state=dict(proto.state), + metadata=dict(proto.metadata), + ) + class PluginContext(BaseModel): """The plugin's context, which lasts a request lifecycle. @@ -887,6 +1056,79 @@ def is_empty(self) -> bool: """ return not (self.state or self.metadata or self.global_context.state) + def model_dump_pb(self): + """Convert to protobuf PluginContext message. + + Returns: + types_pb2.PluginContext: Protobuf message. + + Note: + Lazy imports protobuf to avoid dependency if not needed. + """ + # Third-Party + from google.protobuf import json_format, struct_pb2 + + # First-Party + from mcpgateway.plugins.framework.generated import types_pb2 + + # Convert state dict to map of Struct + state_map = {} + for key, value in self.state.items(): + struct = struct_pb2.Struct() + if isinstance(value, dict): + json_format.ParseDict(value, struct) + else: + struct.update({key: value}) + state_map[key] = struct + + # Convert metadata dict to map of Struct + metadata_map = {} + for key, value in self.metadata.items(): + struct = struct_pb2.Struct() + if isinstance(value, dict): + json_format.ParseDict(value, struct) + else: + struct.update({key: value}) + metadata_map[key] = struct + + return types_pb2.PluginContext( + state=state_map, + global_context=self.global_context.model_dump_pb(), + metadata=metadata_map, + ) + + @classmethod + def model_validate_pb(cls, proto) -> "PluginContext": + """Create from protobuf PluginContext message. + + Args: + proto: types_pb2.PluginContext protobuf message. + + Returns: + PluginContext: Pydantic model instance. + + Note: + Lazy imports protobuf to avoid dependency if not needed. + """ + # Third-Party + from google.protobuf import json_format + + # Convert state map of Struct to dict + state = {} + for key, struct_value in proto.state.items(): + state[key] = json_format.MessageToDict(struct_value) + + # Convert metadata map of Struct to dict + metadata = {} + for key, struct_value in proto.metadata.items(): + metadata[key] = json_format.MessageToDict(struct_value) + + return cls( + state=state, + global_context=GlobalContext.model_validate_pb(proto.global_context), + metadata=metadata, + ) + PluginContextTable = dict[str, PluginContext] diff --git a/plugins/ai_artifacts_normalizer/ai_artifacts_normalizer.py b/plugins/ai_artifacts_normalizer/ai_artifacts_normalizer.py index 215e0e4b6..923bb1ce0 100644 --- a/plugins/ai_artifacts_normalizer/ai_artifacts_normalizer.py +++ b/plugins/ai_artifacts_normalizer/ai_artifacts_normalizer.py @@ -19,9 +19,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, PromptPrehookPayload, PromptPrehookResult, ResourcePostFetchPayload, diff --git a/plugins/altk_json_processor/json_processor.py b/plugins/altk_json_processor/json_processor.py index 4d1cb25fa..df26cedd8 100644 --- a/plugins/altk_json_processor/json_processor.py +++ b/plugins/altk_json_processor/json_processor.py @@ -23,9 +23,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ) diff --git a/plugins/argument_normalizer/argument_normalizer.py b/plugins/argument_normalizer/argument_normalizer.py index e06eb5df8..47e67828c 100644 --- a/plugins/argument_normalizer/argument_normalizer.py +++ b/plugins/argument_normalizer/argument_normalizer.py @@ -27,9 +27,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, PromptPrehookPayload, PromptPrehookResult, ToolPreInvokePayload, diff --git a/plugins/cached_tool_result/cached_tool_result.py b/plugins/cached_tool_result/cached_tool_result.py index d4f3961d0..cce7558b4 100644 --- a/plugins/cached_tool_result/cached_tool_result.py +++ b/plugins/cached_tool_result/cached_tool_result.py @@ -25,9 +25,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, diff --git a/plugins/circuit_breaker/circuit_breaker.py b/plugins/circuit_breaker/circuit_breaker.py index 57d748d41..f9e5de429 100644 --- a/plugins/circuit_breaker/circuit_breaker.py +++ b/plugins/circuit_breaker/circuit_breaker.py @@ -26,10 +26,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, diff --git a/plugins/citation_validator/citation_validator.py b/plugins/citation_validator/citation_validator.py index fc7d71f0f..44fdd4e80 100644 --- a/plugins/citation_validator/citation_validator.py +++ b/plugins/citation_validator/citation_validator.py @@ -24,10 +24,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ToolPostInvokePayload, diff --git a/plugins/code_formatter/code_formatter.py b/plugins/code_formatter/code_formatter.py index fe2d51048..47d3c2d09 100644 --- a/plugins/code_formatter/code_formatter.py +++ b/plugins/code_formatter/code_formatter.py @@ -28,9 +28,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ToolPostInvokePayload, diff --git a/plugins/code_safety_linter/code_safety_linter.py b/plugins/code_safety_linter/code_safety_linter.py index 7c5d80032..c4c17768e 100644 --- a/plugins/code_safety_linter/code_safety_linter.py +++ b/plugins/code_safety_linter/code_safety_linter.py @@ -21,10 +21,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ) diff --git a/plugins/content_moderation/content_moderation.py b/plugins/content_moderation/content_moderation.py index 877654aee..30daed598 100644 --- a/plugins/content_moderation/content_moderation.py +++ b/plugins/content_moderation/content_moderation.py @@ -24,10 +24,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPrehookPayload, PromptPrehookResult, ToolPostInvokePayload, diff --git a/plugins/deny_filter/deny.py b/plugins/deny_filter/deny.py index 7cf7e3790..1b9b1e9b4 100644 --- a/plugins/deny_filter/deny.py +++ b/plugins/deny_filter/deny.py @@ -12,7 +12,14 @@ from pydantic import BaseModel # First-Party -from mcpgateway.plugins.framework import Plugin, PluginConfig, PluginContext, PluginViolation, PromptPrehookPayload, PromptPrehookResult +from mcpgateway.plugins.framework import ( + PluginConfig, + PluginContext, + PluginViolation, + Plugin, + PromptPrehookPayload, + PromptPrehookResult +) from mcpgateway.services.logging_service import LoggingService # Initialize logging service first diff --git a/plugins/external/clamav_server/clamav_plugin.py b/plugins/external/clamav_server/clamav_plugin.py index 7fdce282f..bc18654ee 100644 --- a/plugins/external/clamav_server/clamav_plugin.py +++ b/plugins/external/clamav_server/clamav_plugin.py @@ -31,10 +31,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPosthookPayload, PromptPosthookResult, ResourcePostFetchPayload, diff --git a/plugins/external/llmguard/llmguardplugin/plugin.py b/plugins/external/llmguard/llmguardplugin/plugin.py index 4e52fd90a..d14ad8103 100644 --- a/plugins/external/llmguard/llmguardplugin/plugin.py +++ b/plugins/external/llmguard/llmguardplugin/plugin.py @@ -15,12 +15,12 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginError, PluginErrorModel, PluginViolation, + Plugin, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, diff --git a/plugins/external/opa/opapluginfilter/plugin.py b/plugins/external/opa/opapluginfilter/plugin.py index 408153bed..5d0e7a6de 100644 --- a/plugins/external/opa/opapluginfilter/plugin.py +++ b/plugins/external/opa/opapluginfilter/plugin.py @@ -19,10 +19,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, diff --git a/plugins/file_type_allowlist/file_type_allowlist.py b/plugins/file_type_allowlist/file_type_allowlist.py index aa7b20143..9b2b62ab4 100644 --- a/plugins/file_type_allowlist/file_type_allowlist.py +++ b/plugins/file_type_allowlist/file_type_allowlist.py @@ -22,10 +22,10 @@ # First-Party from mcpgateway.common.models import ResourceContent from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ResourcePreFetchPayload, diff --git a/plugins/harmful_content_detector/harmful_content_detector.py b/plugins/harmful_content_detector/harmful_content_detector.py index 7468cb0d1..3f9d0a48e 100644 --- a/plugins/harmful_content_detector/harmful_content_detector.py +++ b/plugins/harmful_content_detector/harmful_content_detector.py @@ -23,10 +23,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPrehookPayload, PromptPrehookResult, ToolPostInvokePayload, diff --git a/plugins/header_injector/header_injector.py b/plugins/header_injector/header_injector.py index 59173bdc3..daa642155 100644 --- a/plugins/header_injector/header_injector.py +++ b/plugins/header_injector/header_injector.py @@ -22,9 +22,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ResourcePreFetchPayload, ResourcePreFetchResult, ) diff --git a/plugins/html_to_markdown/html_to_markdown.py b/plugins/html_to_markdown/html_to_markdown.py index 9a87dfe23..025a62ce4 100644 --- a/plugins/html_to_markdown/html_to_markdown.py +++ b/plugins/html_to_markdown/html_to_markdown.py @@ -20,9 +20,9 @@ # First-Party from mcpgateway.common.models import ResourceContent from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ) diff --git a/plugins/json_repair/json_repair.py b/plugins/json_repair/json_repair.py index 470209cc4..f246faa1c 100644 --- a/plugins/json_repair/json_repair.py +++ b/plugins/json_repair/json_repair.py @@ -18,9 +18,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ) diff --git a/plugins/license_header_injector/license_header_injector.py b/plugins/license_header_injector/license_header_injector.py index 563cbee56..5fc1e55b3 100644 --- a/plugins/license_header_injector/license_header_injector.py +++ b/plugins/license_header_injector/license_header_injector.py @@ -22,9 +22,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ToolPostInvokePayload, diff --git a/plugins/markdown_cleaner/markdown_cleaner.py b/plugins/markdown_cleaner/markdown_cleaner.py index 16d48c5f9..5e9662302 100644 --- a/plugins/markdown_cleaner/markdown_cleaner.py +++ b/plugins/markdown_cleaner/markdown_cleaner.py @@ -19,9 +19,9 @@ # First-Party from mcpgateway.common.models import Message, PromptResult, ResourceContent, TextContent from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, PromptPosthookPayload, PromptPosthookResult, ResourcePostFetchPayload, diff --git a/plugins/output_length_guard/output_length_guard.py b/plugins/output_length_guard/output_length_guard.py index 7c494987d..4d2884d57 100644 --- a/plugins/output_length_guard/output_length_guard.py +++ b/plugins/output_length_guard/output_length_guard.py @@ -34,10 +34,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ) diff --git a/plugins/pii_filter/pii_filter.py b/plugins/pii_filter/pii_filter.py index 69609c06e..e06c7756a 100644 --- a/plugins/pii_filter/pii_filter.py +++ b/plugins/pii_filter/pii_filter.py @@ -19,10 +19,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, diff --git a/plugins/privacy_notice_injector/privacy_notice_injector.py b/plugins/privacy_notice_injector/privacy_notice_injector.py index f619dbaad..cd45058c3 100644 --- a/plugins/privacy_notice_injector/privacy_notice_injector.py +++ b/plugins/privacy_notice_injector/privacy_notice_injector.py @@ -21,9 +21,9 @@ # First-Party from mcpgateway.common.models import Message, Role, TextContent from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, PromptPosthookPayload, PromptPosthookResult, ) diff --git a/plugins/rate_limiter/rate_limiter.py b/plugins/rate_limiter/rate_limiter.py index 78eccafa4..74ba09a9e 100644 --- a/plugins/rate_limiter/rate_limiter.py +++ b/plugins/rate_limiter/rate_limiter.py @@ -22,10 +22,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPrehookPayload, PromptPrehookResult, ToolPreInvokePayload, diff --git a/plugins/regex_filter/search_replace.py b/plugins/regex_filter/search_replace.py index 79e4fc54f..ef6c59707 100644 --- a/plugins/regex_filter/search_replace.py +++ b/plugins/regex_filter/search_replace.py @@ -16,9 +16,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, diff --git a/plugins/resource_filter/resource_filter.py b/plugins/resource_filter/resource_filter.py index 98121db25..8a25aea4f 100644 --- a/plugins/resource_filter/resource_filter.py +++ b/plugins/resource_filter/resource_filter.py @@ -19,11 +19,11 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginMode, PluginViolation, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ResourcePreFetchPayload, diff --git a/plugins/response_cache_by_prompt/response_cache_by_prompt.py b/plugins/response_cache_by_prompt/response_cache_by_prompt.py index fa7821817..f84ff4d6c 100644 --- a/plugins/response_cache_by_prompt/response_cache_by_prompt.py +++ b/plugins/response_cache_by_prompt/response_cache_by_prompt.py @@ -28,9 +28,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, diff --git a/plugins/retry_with_backoff/retry_with_backoff.py b/plugins/retry_with_backoff/retry_with_backoff.py index ef63ee87f..305da62a4 100644 --- a/plugins/retry_with_backoff/retry_with_backoff.py +++ b/plugins/retry_with_backoff/retry_with_backoff.py @@ -17,9 +17,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ToolPostInvokePayload, diff --git a/plugins/robots_license_guard/robots_license_guard.py b/plugins/robots_license_guard/robots_license_guard.py index 3643688bf..5b7fe3a02 100644 --- a/plugins/robots_license_guard/robots_license_guard.py +++ b/plugins/robots_license_guard/robots_license_guard.py @@ -23,10 +23,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ResourcePreFetchPayload, diff --git a/plugins/safe_html_sanitizer/safe_html_sanitizer.py b/plugins/safe_html_sanitizer/safe_html_sanitizer.py index 1d4364f0f..a6d68cca4 100644 --- a/plugins/safe_html_sanitizer/safe_html_sanitizer.py +++ b/plugins/safe_html_sanitizer/safe_html_sanitizer.py @@ -30,9 +30,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ) diff --git a/plugins/schema_guard/schema_guard.py b/plugins/schema_guard/schema_guard.py index e8962b970..132d21bbf 100644 --- a/plugins/schema_guard/schema_guard.py +++ b/plugins/schema_guard/schema_guard.py @@ -20,10 +20,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, diff --git a/plugins/secrets_detection/secrets_detection.py b/plugins/secrets_detection/secrets_detection.py index 1d2198a6a..fb76c8411 100644 --- a/plugins/secrets_detection/secrets_detection.py +++ b/plugins/secrets_detection/secrets_detection.py @@ -23,10 +23,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPrehookPayload, PromptPrehookResult, ResourcePostFetchPayload, diff --git a/plugins/sql_sanitizer/sql_sanitizer.py b/plugins/sql_sanitizer/sql_sanitizer.py index 5ad84de02..95d39f094 100644 --- a/plugins/sql_sanitizer/sql_sanitizer.py +++ b/plugins/sql_sanitizer/sql_sanitizer.py @@ -26,10 +26,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPrehookPayload, PromptPrehookResult, ToolPreInvokePayload, diff --git a/plugins/summarizer/summarizer.py b/plugins/summarizer/summarizer.py index 9ba229a54..8f4a7990b 100644 --- a/plugins/summarizer/summarizer.py +++ b/plugins/summarizer/summarizer.py @@ -23,9 +23,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ResourcePostFetchPayload, ResourcePostFetchResult, ToolPostInvokePayload, diff --git a/plugins/timezone_translator/timezone_translator.py b/plugins/timezone_translator/timezone_translator.py index af644ca7d..ce1547db3 100644 --- a/plugins/timezone_translator/timezone_translator.py +++ b/plugins/timezone_translator/timezone_translator.py @@ -25,9 +25,9 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, diff --git a/plugins/url_reputation/url_reputation.py b/plugins/url_reputation/url_reputation.py index 35bc2e82d..4ea78b4b0 100644 --- a/plugins/url_reputation/url_reputation.py +++ b/plugins/url_reputation/url_reputation.py @@ -20,10 +20,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, ResourcePreFetchPayload, ResourcePreFetchResult, ) diff --git a/plugins/virus_total_checker/virus_total_checker.py b/plugins/virus_total_checker/virus_total_checker.py index 1754a86c6..4cf5c2824 100644 --- a/plugins/virus_total_checker/virus_total_checker.py +++ b/plugins/virus_total_checker/virus_total_checker.py @@ -31,10 +31,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPosthookPayload, PromptPosthookResult, ResourcePostFetchPayload, diff --git a/plugins/watchdog/watchdog.py b/plugins/watchdog/watchdog.py index e61711f4d..d399e5e94 100644 --- a/plugins/watchdog/watchdog.py +++ b/plugins/watchdog/watchdog.py @@ -23,10 +23,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, diff --git a/plugins/webhook_notification/webhook_notification.py b/plugins/webhook_notification/webhook_notification.py index 37eeb61da..ec1f6a73b 100644 --- a/plugins/webhook_notification/webhook_notification.py +++ b/plugins/webhook_notification/webhook_notification.py @@ -27,10 +27,10 @@ # First-Party from mcpgateway.plugins.framework import ( - Plugin, PluginConfig, PluginContext, PluginViolation, + Plugin, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, diff --git a/protobufs/plugins/schemas/README.md b/protobufs/plugins/schemas/README.md new file mode 100644 index 000000000..416b70355 --- /dev/null +++ b/protobufs/plugins/schemas/README.md @@ -0,0 +1,96 @@ +# ContextForge Protobuf Schemas + +Language-agnostic schema definitions for the ContextForge plugin framework. + +## Why Protobuf? + +Enable plugin development in **multiple languages** (Python, Rust, Go, Java) while maintaining a single source of truth for data structures. Protobuf provides: + +- **Cross-language compatibility** - Write plugins in Rust/Go, integrate with Python gateway +- **Wire protocol** - Efficient serialization for external plugin communication +- **Schema documentation** - Single canonical definition with field requirements +- **Type safety** - Generated code with strong typing for all languages + +## Quick Start + +```bash +# Generate protobuf Python classes +cd protobufs/plugins/schemas +./generate_python.sh + +# Run tests +pytest tests/unit/mcpgateway/plugins/framework/generated/ +``` + +## Architecture + +**Pydantic models** (`mcpgateway/plugins/framework/models.py`) are the canonical Python implementation. + +**Protobuf schemas** (`protobufs/plugins/schemas/`) enable cross-language support (Rust, Go, etc.). + +**Conversion methods** bridge the two: +```python +# Pydantic → Protobuf +proto_msg = pydantic_model.model_dump_pb() + +# Protobuf → Pydantic +pydantic_model = GlobalContext.model_validate_pb(proto_msg) +``` + +## Schema Structure + +``` +protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/ +├── types.proto # Shared types (GlobalContext, PluginViolation, etc.) +├── tools.proto # Tool hook payloads +├── prompts.proto # Prompt hook payloads +├── resources.proto # Resource hook payloads +└── agents.proto # Agent hook payloads +``` + +## Field Requirements + +Protos document field requirements with comments: +- `// REQUIRED` - Must be set +- `// OPTIONAL` - Can be omitted +- `// OPTIONAL - defaults to X` - Default value specified + +## Usage + +**Python (Pydantic)**: +```python +from mcpgateway.plugins.framework.models import GlobalContext + +ctx = GlobalContext(request_id="req-123", user="alice") +``` + +**Cross-language (Protobuf)**: +```python +# Serialize for external plugin +proto_ctx = ctx.model_dump_pb() +serialized = proto_ctx.SerializeToString() + +# Send over wire, receive response... + +# Deserialize +from contextforge.plugins.common import types_pb2 +proto_ctx = types_pb2.GlobalContext() +proto_ctx.ParseFromString(serialized) +ctx = GlobalContext.model_validate_pb(proto_ctx) +``` + +**Other languages**: Generate code from protos using standard tools: +```bash +# Rust +protoc --rust_out=. protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/types.proto + +# Go +protoc --go_out=. protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/types.proto +``` + +## Key Features + +✅ Pydantic models remain canonical (validation, type safety, Python-native) +✅ Protobuf for wire protocol and cross-language serialization +✅ Lazy loading - protobuf only imported when needed +✅ Follows Pydantic conventions (`model_dump_pb()`, `model_validate_pb()`) \ No newline at end of file diff --git a/protobufs/plugins/schemas/buf.yaml b/protobufs/plugins/schemas/buf.yaml new file mode 100644 index 000000000..f7eba0acc --- /dev/null +++ b/protobufs/plugins/schemas/buf.yaml @@ -0,0 +1,25 @@ +# schemas/buf.yaml +# Buf configuration for ContextForge Plugin protobuf schemas +# See: https://docs.buf.build/configuration/v1/buf-yaml + +version: v1 + +# Breaking change detection +breaking: + use: + - FILE + +# Linting configuration +lint: + use: + - DEFAULT + except: + - PACKAGE_VERSION_SUFFIX # We use semantic package names + enum_zero_value_suffix: _UNSPECIFIED + rpc_allow_same_request_response: false + rpc_allow_google_protobuf_empty_requests: true + rpc_allow_google_protobuf_empty_responses: true + service_suffix: Service + +# Module name +name: buf.build/contextforge/plugin-schemas diff --git a/protobufs/plugins/schemas/generate_python.sh b/protobufs/plugins/schemas/generate_python.sh new file mode 100755 index 000000000..4b55d61b2 --- /dev/null +++ b/protobufs/plugins/schemas/generate_python.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# schemas/generate_python.sh +# Generate Python classes from protobuf schemas using betterproto +# +# This script generates Pydantic-compatible Python dataclasses from the +# protobuf schemas defined in this directory. +# +# Requirements: +# - protobuf +# - protoc (Protocol Buffers compiler) +# +# Usage: +# ./generate_python.sh [output_dir] +# +# Default output directory: ../mcpgateway/plugins/framework/generated + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Default output directory +OUTPUT_DIR="${1:-../../..}" + +echo -e "${GREEN}ContextForge Protobuf to Python Generator${NC}" +echo "==========================================" +echo "" + +# Check if google.protobuf is installed +if ! python3 -c "import google.protobuf" 2>/dev/null; then + echo -e "${RED}Error: protobuf is not installed${NC}" + echo "Please install it with: pip install protobuf" + exit 1 +fi + +# Check if protoc is installed +if ! command -v protoc &> /dev/null; then + echo -e "${RED}Error: protoc is not installed${NC}" + echo "Please install Protocol Buffers compiler" + echo " macOS: brew install protobuf" + echo " Ubuntu: apt-get install protobuf-compiler" + echo " Other: https://grpc.io/docs/protoc-installation/" + exit 1 +fi + +echo -e "${GREEN}✓${NC} Dependencies found" +echo "" + +# Create output directory +mkdir -p "$OUTPUT_DIR" +echo -e "${GREEN}Output directory:${NC} $OUTPUT_DIR" +echo "" + +# Generate standard Python protobuf code +echo -e "${GREEN}Generating protobuf Python classes...${NC}" +echo "" + +# Using standard protoc Python generator +# Generates _pb2.py files that can be imported and used with Pydantic conversion methods +# Syntax: protoc --python_out= --proto_path= + +# Generate from all proto files +protoc \ + --python_out="$OUTPUT_DIR" \ + --proto_path="." \ + mcpgateway/plugins/framework/generated/types.proto \ + mcpgateway/plugins/framework/generated/tools.proto \ + mcpgateway/plugins/framework/generated/prompts.proto \ + mcpgateway/plugins/framework/generated/resources.proto \ + mcpgateway/plugins/framework/generated/agents.proto \ + mcpgateway/plugins/framework/generated/http.proto + +echo "" +echo -e "${GREEN}✓${NC} Python classes generated successfully!" +echo "" + +# Create __init__.py files for proper Python package structure +echo -e "${GREEN}Creating package structure...${NC}" + +# Root __init__.py +cat > "$OUTPUT_DIR/mcpgateway/plugins/framework/generated/__init__.py" << 'EOF' +# -*- coding: utf-8 -*- +"""Generated protobuf Python classes for ContextForge plugins. + +This package contains standard protobuf Python classes (_pb2.py files) generated +from protobuf schemas. These are used for cross-language serialization. + +The canonical Python implementation uses Pydantic models in mcpgateway.plugins.framework.models +which have model_dump_pb() and model_validate_pb() methods for conversion. + +Generated using standard protoc from schemas in protobufs/plugins/schemas/ +""" +EOF + +echo -e "${GREEN}✓${NC} Package structure created" +echo "" + +# Print summary +echo -e "${GREEN}Generation Summary${NC}" +echo "==================" +echo "" +echo "Generated files:" +find "$OUTPUT_DIR" -name "*.py" -type f | while read -r file; do + echo " - ${file#$OUTPUT_DIR/}" +done +echo "" + +echo -e "${GREEN}✓${NC} All done!" +echo "" +echo "Generated protobuf Python classes (_pb2.py files)." +echo "" +echo "Usage:" +echo " 1. Use Pydantic models from mcpgateway.plugins.framework.models (canonical)" +echo " 2. Convert to protobuf when needed:" +echo " proto_obj = pydantic_model.model_dump_pb()" +echo " 3. Convert from protobuf:" +echo " pydantic_model = GlobalContext.model_validate_pb(proto_obj)" +echo "" +echo "The protobuf classes are used for:" +echo " - Cross-language serialization (Rust, Go, etc.)" +echo " - Wire protocol for external plugins" +echo " - Schema documentation and validation" diff --git a/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/agents.proto b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/agents.proto new file mode 100644 index 000000000..06d495631 --- /dev/null +++ b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/agents.proto @@ -0,0 +1,56 @@ +// schemas/contextforge/plugins/hooks/agents.proto +// Agent hook payloads and results +// Maps to: mcpgateway/plugins/framework/hooks/agents.py +syntax = "proto3"; + +package contextforge.plugins.hooks; + +import "google/protobuf/struct.proto"; +import "mcpgateway/plugins/framework/generated/types.proto"; + +// Agent hook types +// Maps to: AgentHookType enum (agents.py:25-44) +enum AgentHookType { + AGENT_HOOK_TYPE_UNSPECIFIED = 0; + AGENT_PRE_INVOKE = 1; + AGENT_POST_INVOKE = 2; +} + +// Agent pre-invoke payload +// Maps to: AgentPreInvokePayload (agents.py:47-88) +// Note: messages contains Message objects, using Struct for flexibility +message AgentPreInvokePayload { + string agent_id = 1; // REQUIRED + repeated google.protobuf.Struct messages = 2; // REQUIRED - List[Message] as Struct + repeated string tools = 3; // REQUIRED + contextforge.plugins.common.HttpHeaders headers = 4; // OPTIONAL + string model = 5; // OPTIONAL + string system_prompt = 6; // OPTIONAL + google.protobuf.Struct parameters = 7; // REQUIRED - Dict[str, Any] +} + +// Agent post-invoke payload +// Maps to: AgentPostInvokePayload (agents.py:90-...) +message AgentPostInvokePayload { + string agent_id = 1; // REQUIRED + repeated google.protobuf.Struct messages = 2; // REQUIRED - List[Message] as Struct + repeated google.protobuf.Struct tool_calls = 3; // REQUIRED - List[Dict[str, Any]] as Struct +} + +// Agent pre-invoke result +// Maps to: AgentPreInvokeResult = PluginResult[AgentPreInvokePayload] +message AgentPreInvokeResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + AgentPreInvokePayload modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} + +// Agent post-invoke result +// Maps to: AgentPostInvokeResult = PluginResult[AgentPostInvokePayload] +message AgentPostInvokeResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + AgentPostInvokePayload modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} diff --git a/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/http.proto b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/http.proto new file mode 100644 index 000000000..52ec6e828 --- /dev/null +++ b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/http.proto @@ -0,0 +1,106 @@ +// schemas/contextforge/plugins/hooks/http.proto +// HTTP hook payloads and results +// Maps to: mcpgateway/plugins/framework/hooks/http.py +syntax = "proto3"; + +package contextforge.plugins.hooks; + +import "google/protobuf/struct.proto"; +import "mcpgateway/plugins/framework/generated/types.proto"; + +// HTTP hook types +// Maps to: HttpHookType enum (http.py:63-77) +enum HttpHookType { + HTTP_HOOK_TYPE_UNSPECIFIED = 0; + HTTP_PRE_REQUEST = 1; + HTTP_POST_REQUEST = 2; + HTTP_AUTH_RESOLVE_USER = 3; + HTTP_AUTH_CHECK_PERMISSION = 4; +} + +// HTTP pre-request payload (middleware layer) +// Maps to: HttpPreRequestPayload (http.py:79-99) +message HttpPreRequestPayload { + string path = 1; // REQUIRED + string method = 2; // REQUIRED + string client_host = 3; // OPTIONAL + int32 client_port = 4; // OPTIONAL + contextforge.plugins.common.HttpHeaders headers = 5; // REQUIRED +} + +// HTTP post-request payload (middleware layer) +// Maps to: HttpPostRequestPayload (http.py:101-115) +message HttpPostRequestPayload { + string path = 1; // REQUIRED + string method = 2; // REQUIRED + string client_host = 3; // OPTIONAL + int32 client_port = 4; // OPTIONAL + contextforge.plugins.common.HttpHeaders headers = 5; // REQUIRED + contextforge.plugins.common.HttpHeaders response_headers = 6; // OPTIONAL + int32 status_code = 7; // OPTIONAL +} + +// HTTP auth resolve user payload (auth layer) +// Maps to: HttpAuthResolveUserPayload (http.py:117-135) +message HttpAuthResolveUserPayload { + google.protobuf.Struct credentials = 1; // OPTIONAL - HTTPAuthorizationCredentials serialized + contextforge.plugins.common.HttpHeaders headers = 2; // REQUIRED + string client_host = 3; // OPTIONAL + int32 client_port = 4; // OPTIONAL +} + +// HTTP auth check permission payload (RBAC layer) +// Maps to: HttpAuthCheckPermissionPayload (http.py:137-165) +message HttpAuthCheckPermissionPayload { + string user_email = 1; // REQUIRED + string permission = 2; // REQUIRED + string resource_type = 3; // OPTIONAL + string team_id = 4; // OPTIONAL + bool is_admin = 5; // OPTIONAL - defaults to false + string auth_method = 6; // OPTIONAL + string client_host = 7; // OPTIONAL + string user_agent = 8; // OPTIONAL +} + +// HTTP auth check permission result payload +// Maps to: HttpAuthCheckPermissionResultPayload (http.py:167-179) +message HttpAuthCheckPermissionResultPayload { + bool granted = 1; // REQUIRED + string reason = 2; // OPTIONAL +} + +// HTTP pre-request result +// Maps to: HttpPreRequestResult = PluginResult[HttpHeaderPayload] (http.py:182) +message HttpPreRequestResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + contextforge.plugins.common.HttpHeaders modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} + +// HTTP post-request result +// Maps to: HttpPostRequestResult = PluginResult[HttpHeaderPayload] (http.py:183) +message HttpPostRequestResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + contextforge.plugins.common.HttpHeaders modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} + +// HTTP auth resolve user result +// Maps to: HttpAuthResolveUserResult = PluginResult[dict] (http.py:184) +message HttpAuthResolveUserResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + google.protobuf.Struct modified_payload = 2; // OPTIONAL - user dict (EmailUser serialized) + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} + +// HTTP auth check permission result +// Maps to: HttpAuthCheckPermissionResult = PluginResult[HttpAuthCheckPermissionResultPayload] (http.py:185) +message HttpAuthCheckPermissionResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + HttpAuthCheckPermissionResultPayload modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} diff --git a/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/prompts.proto b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/prompts.proto new file mode 100644 index 000000000..762974a37 --- /dev/null +++ b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/prompts.proto @@ -0,0 +1,50 @@ +// schemas/contextforge/plugins/hooks/prompts.proto +// Prompt hook payloads and results +// Maps to: mcpgateway/plugins/framework/hooks/prompts.py +syntax = "proto3"; + +package contextforge.plugins.hooks; + +import "google/protobuf/struct.proto"; +import "mcpgateway/plugins/framework/generated/types.proto"; + +// Prompt hook types +// Maps to: PromptHookType enum (prompts.py:24-47) +enum PromptHookType { + PROMPT_HOOK_TYPE_UNSPECIFIED = 0; + PROMPT_PRE_FETCH = 1; + PROMPT_POST_FETCH = 2; +} + +// Prompt pre-fetch payload +// Maps to: PromptPrehookPayload (prompts.py:50-75) +message PromptPreFetchPayload { + string prompt_id = 1; // REQUIRED + map args = 2; // REQUIRED +} + +// Prompt post-fetch payload +// Maps to: PromptPosthookPayload (prompts.py:77-103) +// Note: PromptResult contains Message objects, using Struct for flexibility +message PromptPostFetchPayload { + string prompt_id = 1; // REQUIRED + google.protobuf.Struct result = 2; // REQUIRED - PromptResult serialized to Struct +} + +// Prompt pre-fetch result +// Maps to: PromptPrehookResult = PluginResult[PromptPrehookPayload] (prompts.py:105) +message PromptPreFetchResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + PromptPreFetchPayload modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} + +// Prompt post-fetch result +// Maps to: PromptPosthookResult = PluginResult[PromptPosthookPayload] (prompts.py:106) +message PromptPostFetchResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + PromptPostFetchPayload modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} diff --git a/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/resources.proto b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/resources.proto new file mode 100644 index 000000000..52463ad2e --- /dev/null +++ b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/resources.proto @@ -0,0 +1,50 @@ +// schemas/contextforge/plugins/hooks/resources.proto +// Resource hook payloads and results +// Maps to: mcpgateway/plugins/framework/hooks/resources.py +syntax = "proto3"; + +package contextforge.plugins.hooks; + +import "google/protobuf/struct.proto"; +import "mcpgateway/plugins/framework/generated/types.proto"; + +// Resource hook types +// Maps to: ResourceHookType enum (resources.py:21-40) +enum ResourceHookType { + RESOURCE_HOOK_TYPE_UNSPECIFIED = 0; + RESOURCE_PRE_FETCH = 1; + RESOURCE_POST_FETCH = 2; +} + +// Resource pre-fetch payload +// Maps to: ResourcePreFetchPayload (resources.py:43-66) +message ResourcePreFetchPayload { + string uri = 1; // REQUIRED + google.protobuf.Struct metadata = 2; // REQUIRED +} + +// Resource post-fetch payload +// Maps to: ResourcePostFetchPayload (resources.py:68-93) +// Note: content can be complex ResourceContent object, using Struct for flexibility +message ResourcePostFetchPayload { + string uri = 1; // REQUIRED + google.protobuf.Struct content = 2; // REQUIRED - ResourceContent serialized to Struct +} + +// Resource pre-fetch result +// Maps to: ResourcePreFetchResult = PluginResult[ResourcePreFetchPayload] (resources.py:95) +message ResourcePreFetchResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + ResourcePreFetchPayload modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} + +// Resource post-fetch result +// Maps to: ResourcePostFetchResult = PluginResult[ResourcePostFetchPayload] (resources.py:96) +message ResourcePostFetchResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + ResourcePostFetchPayload modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} diff --git a/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/tools.proto b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/tools.proto new file mode 100644 index 000000000..c3f9d460c --- /dev/null +++ b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/tools.proto @@ -0,0 +1,50 @@ +// schemas/contextforge/plugins/hooks/tools.proto +// Tool hook payloads and results +// Maps to: mcpgateway/plugins/framework/hooks/tools.py +syntax = "proto3"; + +package contextforge.plugins.hooks; + +import "google/protobuf/struct.proto"; +import "mcpgateway/plugins/framework/generated/types.proto"; + +// Tool hook types +// Maps to: ToolHookType enum (tools.py:22-41) +enum ToolHookType { + TOOL_HOOK_TYPE_UNSPECIFIED = 0; + TOOL_PRE_INVOKE = 1; + TOOL_POST_INVOKE = 2; +} + +// Tool pre-invoke payload +// Maps to: ToolPreInvokePayload (tools.py:44-72) +message ToolPreInvokePayload { + string name = 1; // REQUIRED + google.protobuf.Struct args = 2; // REQUIRED + contextforge.plugins.common.HttpHeaders headers = 3; // OPTIONAL +} + +// Tool post-invoke payload +// Maps to: ToolPostInvokePayload (tools.py:74-96) +message ToolPostInvokePayload { + string name = 1; // REQUIRED + google.protobuf.Struct result = 2; // REQUIRED +} + +// Tool pre-invoke result +// Maps to: ToolPreInvokeResult = PluginResult[ToolPreInvokePayload] (tools.py:98) +message ToolPreInvokeResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + ToolPreInvokePayload modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} + +// Tool post-invoke result +// Maps to: ToolPostInvokeResult = PluginResult[ToolPostInvokePayload] (tools.py:99) +message ToolPostInvokeResult { + bool continue_processing = 1; // OPTIONAL - defaults to true + ToolPostInvokePayload modified_payload = 2; // OPTIONAL + contextforge.plugins.common.PluginViolation violation = 3; // OPTIONAL + map metadata = 4; // OPTIONAL - defaults to empty dict +} diff --git a/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/types.proto b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/types.proto new file mode 100644 index 000000000..9067fd053 --- /dev/null +++ b/protobufs/plugins/schemas/mcpgateway/plugins/framework/generated/types.proto @@ -0,0 +1,289 @@ +// schemas/contextforge/plugins/common/types.proto +// Common types shared across all ContextForge plugin hooks +// Maps to: mcpgateway/plugins/framework/models.py +syntax = "proto3"; + +package contextforge.plugins.common; + +import "google/protobuf/any.proto"; +import "google/protobuf/struct.proto"; + +// Plugin execution modes +// Maps to: PluginMode enum (models.py:43-68) +enum PluginMode { + PLUGIN_MODE_UNSPECIFIED = 0; + ENFORCE = 1; + ENFORCE_IGNORE_ERROR = 2; + PERMISSIVE = 3; + DISABLED = 4; +} + +// Global context shared across all hook invocations +// Maps to: GlobalContext (models.py:791-826) +message GlobalContext { + string request_id = 1; // REQUIRED + string user = 2; // OPTIONAL + string tenant_id = 3; // OPTIONAL + string server_id = 4; // OPTIONAL + map state = 5; // OPTIONAL - defaults to empty dict + map metadata = 6; // OPTIONAL - defaults to empty dict +} + +// Plugin violation - used when a plugin blocks processing +// Maps to: PluginViolation (models.py:663-717) +message PluginViolation { + string reason = 1; // REQUIRED + string description = 2; // REQUIRED + string code = 3; // REQUIRED + google.protobuf.Struct details = 4; // OPTIONAL - defaults to empty dict + string plugin_name = 5; // OPTIONAL - set by plugin manager +} + +// Plugin condition for conditional execution +// Maps to: PluginCondition (models.py:167-217) +message PluginCondition { + repeated string server_ids = 1; // OPTIONAL + repeated string tenant_ids = 2; // OPTIONAL + repeated string tools = 3; // OPTIONAL + repeated string prompts = 4; // OPTIONAL + repeated string resources = 5; // OPTIONAL + repeated string agents = 6; // OPTIONAL + repeated string user_patterns = 7; // OPTIONAL + repeated string content_types = 8; // OPTIONAL +} + +// HTTP headers +message HttpHeaders { + map headers = 1; // OPTIONAL +} + +// HTTP pre-forwarding payload +// Maps to: HttpPreForwardingPayload (hooks/http.py:74-97) +message HttpPreForwardingPayload { + string target_type = 1; // REQUIRED + string target_id = 2; // REQUIRED + string path = 3; // REQUIRED + string method = 4; // REQUIRED + string client_host = 5; // OPTIONAL + int32 client_port = 6; // OPTIONAL + HttpHeaders headers = 7; // REQUIRED +} + +// HTTP post-forwarding payload +// Maps to: HttpPostForwardingPayload (hooks/http.py:100-112) +message HttpPostForwardingPayload { + string target_type = 1; // REQUIRED + string target_id = 2; // REQUIRED + string path = 3; // REQUIRED + string method = 4; // REQUIRED + string client_host = 5; // OPTIONAL + int32 client_port = 6; // OPTIONAL + HttpHeaders headers = 7; // REQUIRED + HttpHeaders response_headers = 8; // OPTIONAL + int32 status_code = 9; // OPTIONAL +} + +// Generic plugin result for RPC interface (runtime polymorphism) +// Maps to: PluginResult[T] generic class (models.py:753-789) +// +// This message provides a generic container for hook results used in the +// invoke_hook RPC interface. The modified_payload field uses google.protobuf.Any +// to support runtime polymorphism - it can hold any specific payload type +// (ToolPreInvokePayload, PromptPostFetchPayload, etc.) with type information +// embedded via type URL. +// +// For compile-time type safety and documentation, use the specific typed +// result messages (e.g., ToolPreInvokeResult) which repeat these common fields. +message PluginResult { + // Whether to continue processing through the plugin chain + bool continue_processing = 1; // OPTIONAL - defaults to true + + // Modified payload - can be any specific payload type + // Type is preserved via google.protobuf.Any type URL + google.protobuf.Any modified_payload = 2; // OPTIONAL + + // Violation information if processing should be blocked + PluginViolation violation = 3; // OPTIONAL + + // Additional metadata from the plugin + map metadata = 4; // OPTIONAL - defaults to empty dict +} + +// Plugin context for a single request lifecycle +// Maps to: PluginContext (models.py:828-877) +message PluginContext { + // In-memory state for the request + map state = 1; // OPTIONAL - defaults to empty dict + + // Context shared across all plugins + GlobalContext global_context = 2; // REQUIRED + + // Plugin metadata + map metadata = 3; // OPTIONAL - defaults to empty dict +} + +// Plugin error model for exceptions in external plugins +// Maps to: PluginErrorModel (models.py:647-661) +message PluginErrorModel { + // Error message + string message = 1; // REQUIRED + + // Plugin name that raised the error + string plugin_name = 2; // REQUIRED + + // Optional error code + string code = 3; // OPTIONAL - defaults to empty string + + // Additional error details + google.protobuf.Struct details = 4; // OPTIONAL - defaults to empty dict +} + +// Transport type for MCP connections +// Note: This maps to mcpgateway.common.models.TransportType +enum TransportType { + TRANSPORT_TYPE_UNSPECIFIED = 0; + SSE = 1; + STDIO = 2; + STREAMABLEHTTP = 3; +} + +// Base TLS configuration common to client and server +// Maps to: MCPTransportTLSConfigBase (models.py:235-309) +message MCPTransportTLSConfigBase { + string certfile = 1; // OPTIONAL + string keyfile = 2; // OPTIONAL + string ca_bundle = 3; // OPTIONAL + string keyfile_password = 4; // OPTIONAL +} + +// Client-side TLS configuration +// Maps to: MCPClientTLSConfig (models.py:311-354) +message MCPClientTLSConfig { + string certfile = 1; // OPTIONAL + string keyfile = 2; // OPTIONAL + string ca_bundle = 3; // OPTIONAL + string keyfile_password = 4; // OPTIONAL + bool verify = 5; // OPTIONAL - defaults to true + bool check_hostname = 6; // OPTIONAL - defaults to true +} + +// Server-side TLS configuration +// Maps to: MCPServerTLSConfig (models.py:356-397) +message MCPServerTLSConfig { + string certfile = 1; // OPTIONAL + string keyfile = 2; // OPTIONAL + string ca_bundle = 3; // OPTIONAL + string keyfile_password = 4; // OPTIONAL + int32 ssl_cert_reqs = 5; // OPTIONAL - defaults to 2 (REQUIRED) +} + +// Server-side MCP configuration +// Maps to: MCPServerConfig (models.py:400-469) +message MCPServerConfig { + string host = 1; // OPTIONAL - defaults to "0.0.0.0" + int32 port = 2; // OPTIONAL - defaults to 8000 + MCPServerTLSConfig tls = 3; // OPTIONAL +} + +// Client-side MCP configuration +// Maps to: MCPClientConfig (models.py:472-543) +message MCPClientConfig { + TransportType proto = 1; // REQUIRED + string url = 2; // OPTIONAL - required for SSE/STREAMABLEHTTP + string script = 3; // OPTIONAL - required for STDIO + MCPClientTLSConfig tls = 4; // OPTIONAL +} + +// Base template for tool/prompt/resource templates +// Maps to: BaseTemplate (models.py:71-91) +message BaseTemplate { + repeated string context = 1; // OPTIONAL + google.protobuf.Struct extensions = 2; // OPTIONAL +} + +// Tool template +// Maps to: ToolTemplate (models.py:93-117) +message ToolTemplate { + string tool_name = 1; // REQUIRED + repeated string fields = 2; // OPTIONAL + bool result = 3; // OPTIONAL - defaults to false + repeated string context = 4; // OPTIONAL + google.protobuf.Struct extensions = 5; // OPTIONAL +} + +// Prompt template +// Maps to: PromptTemplate (models.py:119-141) +message PromptTemplate { + string prompt_name = 1; // REQUIRED + repeated string fields = 2; // OPTIONAL + bool result = 3; // OPTIONAL - defaults to false + repeated string context = 4; // OPTIONAL + google.protobuf.Struct extensions = 5; // OPTIONAL +} + +// Resource template +// Maps to: ResourceTemplate (models.py:143-165) +message ResourceTemplate { + string resource_uri = 1; // REQUIRED + repeated string fields = 2; // OPTIONAL + bool result = 3; // OPTIONAL - defaults to false + repeated string context = 4; // OPTIONAL + google.protobuf.Struct extensions = 5; // OPTIONAL +} + +// Applied to specification +// Maps to: AppliedTo (models.py:219-233) +message AppliedTo { + repeated ToolTemplate tools = 1; // OPTIONAL + repeated PromptTemplate prompts = 2; // OPTIONAL + repeated ResourceTemplate resources = 3; // OPTIONAL +} + +// Plugin configuration +// Maps to: PluginConfig (models.py:546-624) +message PluginConfig { + string name = 1; // REQUIRED + string description = 2; // OPTIONAL + string author = 3; // OPTIONAL + string kind = 4; // REQUIRED + string namespace = 5; // OPTIONAL + string version = 6; // OPTIONAL + repeated string hooks = 7; // OPTIONAL - defaults to empty list + repeated string tags = 8; // OPTIONAL - defaults to empty list + PluginMode mode = 9; // OPTIONAL - defaults to ENFORCE + int32 priority = 10; // OPTIONAL - defaults to 100 + repeated PluginCondition conditions = 11; // OPTIONAL - defaults to empty list + AppliedTo applied_to = 12; // OPTIONAL + google.protobuf.Struct config = 13; // OPTIONAL + MCPClientConfig mcp = 14; // OPTIONAL - required for external plugins +} + +// Plugin manifest +// Maps to: PluginManifest (models.py:627-645) +message PluginManifest { + string description = 1; // REQUIRED + string author = 2; // REQUIRED + string version = 3; // REQUIRED + repeated string tags = 4; // REQUIRED + repeated string available_hooks = 5; // REQUIRED + google.protobuf.Struct default_config = 6; // REQUIRED +} + +// Global plugin settings +// Maps to: PluginSettings (models.py:719-734) +message PluginSettings { + bool parallel_execution_within_band = 1; // OPTIONAL - defaults to false + int32 plugin_timeout = 2; // OPTIONAL - defaults to 30 + bool fail_on_plugin_error = 3; // OPTIONAL - defaults to false + bool enable_plugin_api = 4; // OPTIONAL - defaults to false + int32 plugin_health_check_interval = 5; // OPTIONAL - defaults to 60 +} + +// Plugin system configuration +// Maps to: Config (models.py:737-750) +message Config { + repeated PluginConfig plugins = 1; // OPTIONAL - defaults to empty list + repeated string plugin_dirs = 2; // OPTIONAL - defaults to empty list + PluginSettings plugin_settings = 3; // REQUIRED + MCPServerConfig server_settings = 4; // OPTIONAL +} diff --git a/tests/unit/mcpgateway/plugins/framework/generated/__init__.py b/tests/unit/mcpgateway/plugins/framework/generated/__init__.py new file mode 100644 index 000000000..7c9c1291b --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/generated/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tests for generated protobuf conversions.""" diff --git a/tests/unit/mcpgateway/plugins/framework/generated/test_agents_protobuf_conversions.py b/tests/unit/mcpgateway/plugins/framework/generated/test_agents_protobuf_conversions.py new file mode 100644 index 000000000..3234acd0f --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/generated/test_agents_protobuf_conversions.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +"""Tests for Agent hook Pydantic to Protobuf conversions. + +This module tests the model_dump_pb() and model_validate_pb() methods +for agent hook payload classes. +""" + +# Third-Party +import pytest + +# First-Party +from mcpgateway.common.models import Message, Role, TextContent +from mcpgateway.plugins.framework.hooks.agents import ( + AgentPostInvokePayload, + AgentPreInvokePayload, +) + +# Check if protobuf is available +try: + import google.protobuf # noqa: F401 + + PROTOBUF_AVAILABLE = True +except ImportError: + PROTOBUF_AVAILABLE = False + +pytestmark = pytest.mark.skipif(not PROTOBUF_AVAILABLE, reason="protobuf not installed") + + +class TestAgentPreInvokePayloadConversion: + """Test AgentPreInvokePayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion(self): + """Test basic AgentPreInvokePayload conversion to protobuf and back.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Hello")) + payload = AgentPreInvokePayload(agent_id="agent-123", messages=[msg]) + + # Convert to protobuf + proto_payload = payload.model_dump_pb() + + # Verify protobuf fields + assert proto_payload.agent_id == "agent-123" + assert len(proto_payload.messages) == 1 + + # Convert back to Pydantic + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + # Verify restoration + assert restored.agent_id == payload.agent_id + assert len(restored.messages) == 1 + assert restored.messages[0].content.text == "Hello" + + def test_with_empty_messages(self): + """Test AgentPreInvokePayload with empty messages list.""" + payload = AgentPreInvokePayload(agent_id="agent-456", messages=[]) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.agent_id == "agent-456" + assert len(restored.messages) == 0 + + def test_with_tools(self): + """Test AgentPreInvokePayload with tools list.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Query")) + payload = AgentPreInvokePayload( + agent_id="agent-789", + messages=[msg], + tools=["search", "calculator", "weather"], + ) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert len(restored.tools) == 3 + assert "search" in restored.tools + assert "calculator" in restored.tools + assert "weather" in restored.tools + + def test_with_headers(self): + """Test AgentPreInvokePayload with HTTP headers.""" + from mcpgateway.plugins.framework.hooks.http import HttpHeaderPayload + + msg = Message(role=Role.USER, content=TextContent(type="text", text="Request")) + headers = HttpHeaderPayload({"Authorization": "Bearer token", "X-Request-ID": "req-123"}) + payload = AgentPreInvokePayload( + agent_id="agent-api", + messages=[msg], + headers=headers, + ) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.headers["Authorization"] == "Bearer token" + assert restored.headers["X-Request-ID"] == "req-123" + + def test_with_model_override(self): + """Test AgentPreInvokePayload with model override.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Test")) + payload = AgentPreInvokePayload( + agent_id="agent-model", + messages=[msg], + model="claude-3-5-sonnet-20241022", + ) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.model == "claude-3-5-sonnet-20241022" + + def test_with_system_prompt(self): + """Test AgentPreInvokePayload with system prompt.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Help")) + payload = AgentPreInvokePayload( + agent_id="agent-sys", + messages=[msg], + system_prompt="You are a helpful assistant specialized in Python programming.", + ) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert "helpful assistant" in restored.system_prompt + assert "Python programming" in restored.system_prompt + + def test_with_parameters(self): + """Test AgentPreInvokePayload with LLM parameters.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Generate")) + payload = AgentPreInvokePayload( + agent_id="agent-params", + messages=[msg], + parameters={"temperature": 0.7, "max_tokens": 1000, "top_p": 0.9}, + ) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert "temperature" in restored.parameters + assert "max_tokens" in restored.parameters + assert "top_p" in restored.parameters + + def test_with_multiple_messages(self): + """Test AgentPreInvokePayload with conversation history.""" + messages = [ + Message(role=Role.USER, content=TextContent(type="text", text="Hello")), + Message(role=Role.ASSISTANT, content=TextContent(type="text", text="Hi there!")), + Message(role=Role.USER, content=TextContent(type="text", text="How are you?")), + ] + payload = AgentPreInvokePayload(agent_id="agent-conv", messages=messages) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert len(restored.messages) == 3 + assert restored.messages[0].role == Role.USER + assert restored.messages[1].role == Role.ASSISTANT + assert restored.messages[2].role == Role.USER + + def test_roundtrip_conversion(self): + """Test that multiple roundtrips maintain data integrity.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Test")) + original = AgentPreInvokePayload( + agent_id="agent-roundtrip", + messages=[msg], + tools=["tool1", "tool2"], + model="test-model", + parameters={"key": "value"}, + ) + + proto1 = original.model_dump_pb() + restored1 = AgentPreInvokePayload.model_validate_pb(proto1) + proto2 = restored1.model_dump_pb() + restored2 = AgentPreInvokePayload.model_validate_pb(proto2) + + assert original.agent_id == restored2.agent_id + assert len(restored2.messages) == 1 + assert len(restored2.tools) == 2 + assert restored2.model == "test-model" + + +class TestAgentPostInvokePayloadConversion: + """Test AgentPostInvokePayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion(self): + """Test basic AgentPostInvokePayload conversion.""" + msg = Message(role=Role.ASSISTANT, content=TextContent(type="text", text="Response")) + payload = AgentPostInvokePayload(agent_id="agent-123", messages=[msg]) + + proto_payload = payload.model_dump_pb() + assert proto_payload.agent_id == "agent-123" + assert len(proto_payload.messages) == 1 + + restored = AgentPostInvokePayload.model_validate_pb(proto_payload) + assert restored.agent_id == "agent-123" + assert restored.messages[0].content.text == "Response" + + def test_with_empty_messages(self): + """Test AgentPostInvokePayload with empty messages.""" + payload = AgentPostInvokePayload(agent_id="agent-empty", messages=[]) + + proto_payload = payload.model_dump_pb() + restored = AgentPostInvokePayload.model_validate_pb(proto_payload) + + assert len(restored.messages) == 0 + + def test_with_tool_calls(self): + """Test AgentPostInvokePayload with tool calls.""" + msg = Message(role=Role.ASSISTANT, content=TextContent(type="text", text="Let me search")) + tool_calls = [ + {"name": "search", "arguments": {"query": "Python tutorials"}}, + {"name": "calculator", "arguments": {"operation": "add", "a": 5, "b": 3}}, + ] + payload = AgentPostInvokePayload(agent_id="agent-tools", messages=[msg], tool_calls=tool_calls) + + proto_payload = payload.model_dump_pb() + restored = AgentPostInvokePayload.model_validate_pb(proto_payload) + + assert len(restored.tool_calls) == 2 + assert restored.tool_calls[0]["name"] == "search" + assert restored.tool_calls[1]["name"] == "calculator" + assert restored.tool_calls[1]["arguments"]["a"] == 5 + + def test_with_multiple_messages(self): + """Test AgentPostInvokePayload with multiple response messages.""" + messages = [ + Message(role=Role.ASSISTANT, content=TextContent(type="text", text="First part")), + Message(role=Role.ASSISTANT, content=TextContent(type="text", text="Second part")), + ] + payload = AgentPostInvokePayload(agent_id="agent-multi", messages=messages) + + proto_payload = payload.model_dump_pb() + restored = AgentPostInvokePayload.model_validate_pb(proto_payload) + + assert len(restored.messages) == 2 + assert restored.messages[0].content.text == "First part" + assert restored.messages[1].content.text == "Second part" + + def test_with_complex_tool_calls(self): + """Test AgentPostInvokePayload with complex nested tool calls.""" + msg = Message(role=Role.ASSISTANT, content=TextContent(type="text", text="Processing")) + tool_calls = [ + { + "name": "api_call", + "arguments": { + "endpoint": "/v1/data", + "method": "POST", + "body": {"query": "test", "filters": {"active": True}}, + }, + } + ] + payload = AgentPostInvokePayload(agent_id="agent-complex", messages=[msg], tool_calls=tool_calls) + + proto_payload = payload.model_dump_pb() + restored = AgentPostInvokePayload.model_validate_pb(proto_payload) + + assert len(restored.tool_calls) == 1 + assert "body" in restored.tool_calls[0]["arguments"] + assert "filters" in restored.tool_calls[0]["arguments"]["body"] + + def test_without_tool_calls(self): + """Test AgentPostInvokePayload without tool calls (None).""" + msg = Message(role=Role.ASSISTANT, content=TextContent(type="text", text="Direct answer")) + payload = AgentPostInvokePayload(agent_id="agent-direct", messages=[msg], tool_calls=None) + + proto_payload = payload.model_dump_pb() + restored = AgentPostInvokePayload.model_validate_pb(proto_payload) + + assert restored.tool_calls is None + + def test_roundtrip_conversion(self): + """Test that multiple roundtrips maintain data integrity.""" + msg = Message(role=Role.ASSISTANT, content=TextContent(type="text", text="Response")) + tool_calls = [{"name": "test_tool", "arguments": {"arg": "value"}}] + original = AgentPostInvokePayload(agent_id="agent-roundtrip", messages=[msg], tool_calls=tool_calls) + + proto1 = original.model_dump_pb() + restored1 = AgentPostInvokePayload.model_validate_pb(proto1) + proto2 = restored1.model_dump_pb() + restored2 = AgentPostInvokePayload.model_validate_pb(proto2) + + assert original.agent_id == restored2.agent_id + assert len(restored2.messages) == 1 + assert len(restored2.tool_calls) == 1 + + +class TestAgentPayloadEdgeCases: + """Test edge cases for agent payload conversions.""" + + def test_empty_agent_id(self): + """Test with empty agent ID.""" + payload = AgentPreInvokePayload(agent_id="", messages=[]) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.agent_id == "" + + def test_agent_id_with_special_characters(self): + """Test agent ID with special characters.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Test")) + payload = AgentPreInvokePayload(agent_id="agent-v2.0_prod:us-east-1", messages=[msg]) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.agent_id == "agent-v2.0_prod:us-east-1" + + def test_large_tools_list(self): + """Test with large tools list.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Query")) + tools = [f"tool_{i}" for i in range(100)] + payload = AgentPreInvokePayload(agent_id="agent-many-tools", messages=[msg], tools=tools) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert len(restored.tools) == 100 + assert "tool_50" in restored.tools + + def test_long_conversation_history(self): + """Test with long conversation history.""" + messages = [ + Message(role=Role.USER if i % 2 == 0 else Role.ASSISTANT, content=TextContent(type="text", text=f"Message {i}")) + for i in range(50) + ] + payload = AgentPreInvokePayload(agent_id="agent-long-conv", messages=messages) + + proto_payload = payload.model_dump_pb() + restored = AgentPreInvokePayload.model_validate_pb(proto_payload) + + assert len(restored.messages) == 50 + assert restored.messages[25].content.text == "Message 25" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/unit/mcpgateway/plugins/framework/generated/test_http_protobuf_conversions.py b/tests/unit/mcpgateway/plugins/framework/generated/test_http_protobuf_conversions.py new file mode 100644 index 000000000..0c153825e --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/generated/test_http_protobuf_conversions.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +"""Tests for HTTP hook Pydantic to Protobuf conversions. + +This module tests the model_dump_pb() and model_validate_pb() methods +for HTTP hook payload classes. +""" + +# Third-Party +import pytest + +# First-Party +from mcpgateway.plugins.framework.hooks.http import ( + HttpAuthCheckPermissionPayload, + HttpAuthCheckPermissionResultPayload, + HttpAuthResolveUserPayload, + HttpHeaderPayload, + HttpPostRequestPayload, + HttpPreRequestPayload, +) + +# Check if protobuf is available +try: + import google.protobuf # noqa: F401 + + PROTOBUF_AVAILABLE = True +except ImportError: + PROTOBUF_AVAILABLE = False + +pytestmark = pytest.mark.skipif(not PROTOBUF_AVAILABLE, reason="protobuf not installed") + + +class TestHttpPreRequestPayloadConversion: + """Test HttpPreRequestPayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion(self): + """Test basic HttpPreRequestPayload conversion to protobuf and back.""" + headers = HttpHeaderPayload({"Authorization": "Bearer token123", "Content-Type": "application/json"}) + payload = HttpPreRequestPayload( + path="/api/v1/tools", + method="GET", + client_host="192.168.1.100", + client_port=54321, + headers=headers, + ) + + # Convert to protobuf + proto_payload = payload.model_dump_pb() + + # Verify protobuf fields + assert proto_payload.path == "/api/v1/tools" + assert proto_payload.method == "GET" + assert proto_payload.client_host == "192.168.1.100" + assert proto_payload.client_port == 54321 + + # Convert back to Pydantic + restored = HttpPreRequestPayload.model_validate_pb(proto_payload) + + # Verify restoration + assert restored.path == payload.path + assert restored.method == payload.method + assert restored.client_host == payload.client_host + assert restored.client_port == payload.client_port + assert restored.headers["Authorization"] == "Bearer token123" + + def test_with_optional_fields_none(self): + """Test HttpPreRequestPayload with optional fields as None.""" + headers = HttpHeaderPayload({"X-Custom-Header": "value"}) + payload = HttpPreRequestPayload( + path="/test", + method="POST", + client_host=None, + client_port=None, + headers=headers, + ) + + proto_payload = payload.model_dump_pb() + restored = HttpPreRequestPayload.model_validate_pb(proto_payload) + + assert restored.path == "/test" + assert restored.method == "POST" + assert restored.client_host is None + assert restored.client_port is None + + def test_roundtrip_conversion(self): + """Test multiple roundtrip conversions maintain data integrity.""" + headers = HttpHeaderPayload({"User-Agent": "TestAgent/1.0"}) + payload = HttpPreRequestPayload( + path="/api/tools/invoke", + method="POST", + client_host="10.0.0.1", + client_port=8080, + headers=headers, + ) + + # Multiple roundtrips + for _ in range(3): + proto_payload = payload.model_dump_pb() + payload = HttpPreRequestPayload.model_validate_pb(proto_payload) + + assert payload.path == "/api/tools/invoke" + assert payload.method == "POST" + assert payload.client_host == "10.0.0.1" + + +class TestHttpPostRequestPayloadConversion: + """Test HttpPostRequestPayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion(self): + """Test basic HttpPostRequestPayload conversion to protobuf and back.""" + headers = HttpHeaderPayload({"Authorization": "Bearer token"}) + response_headers = HttpHeaderPayload({"Content-Type": "application/json", "X-Request-ID": "req-123"}) + payload = HttpPostRequestPayload( + path="/api/v1/tools", + method="POST", + client_host="192.168.1.100", + client_port=54321, + headers=headers, + response_headers=response_headers, + status_code=200, + ) + + proto_payload = payload.model_dump_pb() + restored = HttpPostRequestPayload.model_validate_pb(proto_payload) + + assert restored.path == payload.path + assert restored.method == payload.method + assert restored.status_code == 200 + assert restored.response_headers["Content-Type"] == "application/json" + assert restored.response_headers["X-Request-ID"] == "req-123" + + def test_with_error_status_code(self): + """Test HttpPostRequestPayload with error status code.""" + headers = HttpHeaderPayload({}) + response_headers = HttpHeaderPayload({"Content-Type": "application/json"}) + payload = HttpPostRequestPayload( + path="/api/error", + method="GET", + headers=headers, + response_headers=response_headers, + status_code=500, + ) + + proto_payload = payload.model_dump_pb() + restored = HttpPostRequestPayload.model_validate_pb(proto_payload) + + assert restored.status_code == 500 + + def test_without_response_headers(self): + """Test HttpPostRequestPayload without response headers.""" + headers = HttpHeaderPayload({"Authorization": "Bearer token"}) + payload = HttpPostRequestPayload( + path="/api/test", + method="GET", + headers=headers, + response_headers=None, + status_code=204, + ) + + proto_payload = payload.model_dump_pb() + restored = HttpPostRequestPayload.model_validate_pb(proto_payload) + + assert restored.response_headers is None + assert restored.status_code == 204 + + +class TestHttpAuthResolveUserPayloadConversion: + """Test HttpAuthResolveUserPayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion_with_credentials(self): + """Test HttpAuthResolveUserPayload with credentials.""" + headers = HttpHeaderPayload({"Authorization": "Bearer token123"}) + credentials = {"scheme": "Bearer", "credentials": "token123"} + payload = HttpAuthResolveUserPayload( + credentials=credentials, + headers=headers, + client_host="192.168.1.100", + client_port=54321, + ) + + proto_payload = payload.model_dump_pb() + restored = HttpAuthResolveUserPayload.model_validate_pb(proto_payload) + + assert restored.credentials["scheme"] == "Bearer" + assert restored.credentials["credentials"] == "token123" + assert restored.client_host == "192.168.1.100" + assert restored.client_port == 54321 + + def test_without_credentials(self): + """Test HttpAuthResolveUserPayload without credentials.""" + headers = HttpHeaderPayload({"X-API-Key": "secret123"}) + payload = HttpAuthResolveUserPayload( + credentials=None, + headers=headers, + client_host="10.0.0.1", + client_port=443, + ) + + proto_payload = payload.model_dump_pb() + restored = HttpAuthResolveUserPayload.model_validate_pb(proto_payload) + + assert restored.credentials is None + assert restored.headers["X-API-Key"] == "secret123" + + def test_with_custom_headers(self): + """Test HttpAuthResolveUserPayload with custom authentication headers.""" + headers = HttpHeaderPayload( + {"X-Client-Cert-DN": "CN=user,O=org", "X-LDAP-Token": "ldap-token-123", "X-Correlation-ID": "corr-456"} + ) + payload = HttpAuthResolveUserPayload( + credentials=None, + headers=headers, + client_host="192.168.1.50", + client_port=8443, + ) + + proto_payload = payload.model_dump_pb() + restored = HttpAuthResolveUserPayload.model_validate_pb(proto_payload) + + assert restored.headers["X-Client-Cert-DN"] == "CN=user,O=org" + assert restored.headers["X-LDAP-Token"] == "ldap-token-123" + assert restored.headers["X-Correlation-ID"] == "corr-456" + + +class TestHttpAuthCheckPermissionPayloadConversion: + """Test HttpAuthCheckPermissionPayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion(self): + """Test basic HttpAuthCheckPermissionPayload conversion.""" + payload = HttpAuthCheckPermissionPayload( + user_email="user@example.com", + permission="tools.read", + resource_type="tool", + team_id="team-123", + is_admin=False, + auth_method="simple_token", + client_host="192.168.1.100", + user_agent="TestClient/1.0", + ) + + proto_payload = payload.model_dump_pb() + restored = HttpAuthCheckPermissionPayload.model_validate_pb(proto_payload) + + assert restored.user_email == "user@example.com" + assert restored.permission == "tools.read" + assert restored.resource_type == "tool" + assert restored.team_id == "team-123" + assert restored.is_admin is False + assert restored.auth_method == "simple_token" + assert restored.client_host == "192.168.1.100" + assert restored.user_agent == "TestClient/1.0" + + def test_with_admin_user(self): + """Test HttpAuthCheckPermissionPayload with admin user.""" + payload = HttpAuthCheckPermissionPayload( + user_email="admin@example.com", + permission="servers.write", + resource_type="server", + is_admin=True, + auth_method="jwt", + ) + + proto_payload = payload.model_dump_pb() + restored = HttpAuthCheckPermissionPayload.model_validate_pb(proto_payload) + + assert restored.user_email == "admin@example.com" + assert restored.is_admin is True + assert restored.permission == "servers.write" + + def test_with_optional_fields_none(self): + """Test HttpAuthCheckPermissionPayload with optional fields as None.""" + payload = HttpAuthCheckPermissionPayload( + user_email="user@example.com", + permission="prompts.read", + resource_type=None, + team_id=None, + is_admin=False, + auth_method=None, + client_host=None, + user_agent=None, + ) + + proto_payload = payload.model_dump_pb() + restored = HttpAuthCheckPermissionPayload.model_validate_pb(proto_payload) + + assert restored.user_email == "user@example.com" + assert restored.permission == "prompts.read" + assert restored.resource_type is None + assert restored.team_id is None + assert restored.auth_method is None + + +class TestHttpAuthCheckPermissionResultPayloadConversion: + """Test HttpAuthCheckPermissionResultPayload Pydantic <-> Protobuf conversion.""" + + def test_granted_permission(self): + """Test HttpAuthCheckPermissionResultPayload with granted permission.""" + payload = HttpAuthCheckPermissionResultPayload(granted=True, reason="API key has valid permissions") + + proto_payload = payload.model_dump_pb() + restored = HttpAuthCheckPermissionResultPayload.model_validate_pb(proto_payload) + + assert restored.granted is True + assert restored.reason == "API key has valid permissions" + + def test_denied_permission(self): + """Test HttpAuthCheckPermissionResultPayload with denied permission.""" + payload = HttpAuthCheckPermissionResultPayload(granted=False, reason="Insufficient permissions") + + proto_payload = payload.model_dump_pb() + restored = HttpAuthCheckPermissionResultPayload.model_validate_pb(proto_payload) + + assert restored.granted is False + assert restored.reason == "Insufficient permissions" + + def test_without_reason(self): + """Test HttpAuthCheckPermissionResultPayload without reason.""" + payload = HttpAuthCheckPermissionResultPayload(granted=True, reason=None) + + proto_payload = payload.model_dump_pb() + restored = HttpAuthCheckPermissionResultPayload.model_validate_pb(proto_payload) + + assert restored.granted is True + assert restored.reason is None + + def test_roundtrip_conversion(self): + """Test multiple roundtrip conversions maintain data integrity.""" + payload = HttpAuthCheckPermissionResultPayload(granted=False, reason="Token expired") + + # Multiple roundtrips + for _ in range(3): + proto_payload = payload.model_dump_pb() + payload = HttpAuthCheckPermissionResultPayload.model_validate_pb(proto_payload) + + assert payload.granted is False + assert payload.reason == "Token expired" diff --git a/tests/unit/mcpgateway/plugins/framework/generated/test_prompts_protobuf_conversions.py b/tests/unit/mcpgateway/plugins/framework/generated/test_prompts_protobuf_conversions.py new file mode 100644 index 000000000..0dc1e978a --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/generated/test_prompts_protobuf_conversions.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +"""Tests for Prompt hook Pydantic to Protobuf conversions. + +This module tests the model_dump_pb() and model_validate_pb() methods +for prompt hook payload classes. +""" + +# Third-Party +import pytest + +# First-Party +from mcpgateway.common.models import Message, PromptResult, Role, TextContent +from mcpgateway.plugins.framework.hooks.prompts import ( + PromptPosthookPayload, + PromptPrehookPayload, +) + +# Check if protobuf is available +try: + import google.protobuf # noqa: F401 + + PROTOBUF_AVAILABLE = True +except ImportError: + PROTOBUF_AVAILABLE = False + +pytestmark = pytest.mark.skipif(not PROTOBUF_AVAILABLE, reason="protobuf not installed") + + +class TestPromptPrehookPayloadConversion: + """Test PromptPrehookPayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion(self): + """Test basic PromptPrehookPayload conversion to protobuf and back.""" + payload = PromptPrehookPayload( + prompt_id="prompt-123", + args={"user": "alice", "context": "testing"}, + ) + + # Convert to protobuf + proto_payload = payload.model_dump_pb() + + # Verify protobuf fields + assert proto_payload.prompt_id == "prompt-123" + + # Convert back to Pydantic + restored = PromptPrehookPayload.model_validate_pb(proto_payload) + + # Verify restoration + assert restored.prompt_id == payload.prompt_id + assert restored.args == payload.args + assert restored == payload + + def test_with_empty_args(self): + """Test PromptPrehookPayload with empty args.""" + payload = PromptPrehookPayload(prompt_id="prompt-456") + + proto_payload = payload.model_dump_pb() + restored = PromptPrehookPayload.model_validate_pb(proto_payload) + + assert restored.prompt_id == "prompt-456" + assert restored.args == {} + + def test_with_multiple_args(self): + """Test PromptPrehookPayload with multiple arguments.""" + payload = PromptPrehookPayload( + prompt_id="prompt-789", + args={ + "name": "Bob", + "time": "morning", + "location": "office", + "mood": "happy", + }, + ) + + proto_payload = payload.model_dump_pb() + restored = PromptPrehookPayload.model_validate_pb(proto_payload) + + assert restored.prompt_id == "prompt-789" + assert restored.args["name"] == "Bob" + assert restored.args["time"] == "morning" + assert len(restored.args) == 4 + + def test_roundtrip_conversion(self): + """Test that multiple roundtrips maintain data integrity.""" + original = PromptPrehookPayload( + prompt_id="roundtrip", + args={"key1": "value1", "key2": "value2"}, + ) + + proto1 = original.model_dump_pb() + restored1 = PromptPrehookPayload.model_validate_pb(proto1) + proto2 = restored1.model_dump_pb() + restored2 = PromptPrehookPayload.model_validate_pb(proto2) + + assert original == restored1 == restored2 + + +class TestPromptPosthookPayloadConversion: + """Test PromptPosthookPayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion(self): + """Test basic PromptPosthookPayload conversion with PromptResult.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Hello World")) + result = PromptResult(messages=[msg]) + payload = PromptPosthookPayload(prompt_id="prompt-123", result=result) + + proto_payload = payload.model_dump_pb() + assert proto_payload.prompt_id == "prompt-123" + + restored = PromptPosthookPayload.model_validate_pb(proto_payload) + assert restored.prompt_id == "prompt-123" + assert len(restored.result.messages) == 1 + assert restored.result.messages[0].content.text == "Hello World" + + def test_with_multiple_messages(self): + """Test PromptPosthookPayload with multiple messages.""" + msg1 = Message(role=Role.USER, content=TextContent(type="text", text="Question")) + msg2 = Message(role=Role.ASSISTANT, content=TextContent(type="text", text="Answer")) + result = PromptResult(messages=[msg1, msg2]) + payload = PromptPosthookPayload(prompt_id="prompt-456", result=result) + + proto_payload = payload.model_dump_pb() + restored = PromptPosthookPayload.model_validate_pb(proto_payload) + + assert len(restored.result.messages) == 2 + assert restored.result.messages[0].role == Role.USER + assert restored.result.messages[1].role == Role.ASSISTANT + + def test_with_assistant_message(self): + """Test PromptPosthookPayload with assistant message.""" + msg = Message(role=Role.ASSISTANT, content=TextContent(type="text", text="I am a helpful assistant")) + result = PromptResult(messages=[msg]) + payload = PromptPosthookPayload(prompt_id="assistant-prompt", result=result) + + proto_payload = payload.model_dump_pb() + restored = PromptPosthookPayload.model_validate_pb(proto_payload) + + assert restored.result.messages[0].role == Role.ASSISTANT + assert "helpful assistant" in restored.result.messages[0].content.text + + def test_roundtrip_conversion(self): + """Test that multiple roundtrips maintain data integrity.""" + msg = Message(role=Role.USER, content=TextContent(type="text", text="Test message")) + result = PromptResult(messages=[msg]) + original = PromptPosthookPayload(prompt_id="roundtrip", result=result) + + proto1 = original.model_dump_pb() + restored1 = PromptPosthookPayload.model_validate_pb(proto1) + proto2 = restored1.model_dump_pb() + restored2 = PromptPosthookPayload.model_validate_pb(proto2) + + assert original.prompt_id == restored2.prompt_id + assert len(restored2.result.messages) == 1 + assert restored2.result.messages[0].content.text == "Test message" + + +class TestPromptPayloadEdgeCases: + """Test edge cases for prompt payload conversions.""" + + def test_empty_prompt_id(self): + """Test with empty prompt ID.""" + payload = PromptPrehookPayload(prompt_id="", args={}) + + proto_payload = payload.model_dump_pb() + restored = PromptPrehookPayload.model_validate_pb(proto_payload) + + assert restored.prompt_id == "" + + def test_prompt_id_with_special_characters(self): + """Test prompt ID with special characters.""" + payload = PromptPrehookPayload( + prompt_id="my-prompt_v2.0:test", + args={"key": "value"}, + ) + + proto_payload = payload.model_dump_pb() + restored = PromptPrehookPayload.model_validate_pb(proto_payload) + + assert restored.prompt_id == "my-prompt_v2.0:test" + + def test_large_args_dict(self): + """Test with large arguments dictionary.""" + large_args = {f"arg_{i}": f"value_{i}" for i in range(50)} + payload = PromptPrehookPayload(prompt_id="bulk-prompt", args=large_args) + + proto_payload = payload.model_dump_pb() + restored = PromptPrehookPayload.model_validate_pb(proto_payload) + + assert len(restored.args) == 50 + assert restored.args["arg_25"] == "value_25" + + def test_empty_message_list(self): + """Test PromptPosthookPayload with empty message list.""" + result = PromptResult(messages=[]) + payload = PromptPosthookPayload(prompt_id="empty", result=result) + + proto_payload = payload.model_dump_pb() + restored = PromptPosthookPayload.model_validate_pb(proto_payload) + + assert len(restored.result.messages) == 0 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/unit/mcpgateway/plugins/framework/generated/test_protobuf_conversions.py b/tests/unit/mcpgateway/plugins/framework/generated/test_protobuf_conversions.py new file mode 100644 index 000000000..9a6ac5e86 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/generated/test_protobuf_conversions.py @@ -0,0 +1,391 @@ +# -*- coding: utf-8 -*- +"""Tests for Pydantic to Protobuf conversions. + +This module tests the model_dump_pb() and model_validate_pb() methods +for converting between Pydantic models and protobuf messages. +""" + +# Standard +from typing import Any + +# Third-Party +import pytest + +# First-Party +from mcpgateway.plugins.framework.models import ( + GlobalContext, + PluginContext, + PluginResult, + PluginViolation, +) + +# Check if protobuf is available +try: + import google.protobuf # noqa: F401 + + PROTOBUF_AVAILABLE = True +except ImportError: + PROTOBUF_AVAILABLE = False + +pytestmark = pytest.mark.skipif(not PROTOBUF_AVAILABLE, reason="protobuf not installed") + + +class TestGlobalContextConversion: + """Test GlobalContext Pydantic <-> Protobuf conversion.""" + + def test_global_context_basic_conversion(self): + """Test basic GlobalContext conversion to protobuf and back.""" + # Create Pydantic model + ctx = GlobalContext( + request_id="req-123", + user="alice", + tenant_id="tenant-1", + server_id="server-1", + ) + + # Convert to protobuf + proto_ctx = ctx.model_dump_pb() + + # Verify protobuf fields + assert proto_ctx.request_id == "req-123" + assert proto_ctx.user == "alice" + assert proto_ctx.tenant_id == "tenant-1" + assert proto_ctx.server_id == "server-1" + + # Convert back to Pydantic + restored = GlobalContext.model_validate_pb(proto_ctx) + + # Verify restoration + assert restored.request_id == ctx.request_id + assert restored.user == ctx.user + assert restored.tenant_id == ctx.tenant_id + assert restored.server_id == ctx.server_id + assert restored == ctx + + def test_global_context_with_optional_fields(self): + """Test GlobalContext with None values converts correctly.""" + ctx = GlobalContext(request_id="req-456") + + # Convert to protobuf + proto_ctx = ctx.model_dump_pb() + + # Convert back to Pydantic + restored = GlobalContext.model_validate_pb(proto_ctx) + + assert restored.request_id == "req-456" + assert restored.user is None + assert restored.tenant_id is None + assert restored.server_id is None + assert restored == ctx + + def test_global_context_with_state_and_metadata(self): + """Test GlobalContext with state and metadata.""" + ctx = GlobalContext( + request_id="req-789", + state={"key1": "value1", "key2": "value2"}, + metadata={"meta1": "data1"}, + ) + + # Convert to protobuf + proto_ctx = ctx.model_dump_pb() + + # Convert back to Pydantic + restored = GlobalContext.model_validate_pb(proto_ctx) + + assert restored.request_id == ctx.request_id + assert restored.state == ctx.state + assert restored.metadata == ctx.metadata + assert restored == ctx + + def test_global_context_roundtrip(self): + """Test that multiple roundtrips maintain data integrity.""" + original = GlobalContext( + request_id="req-multi", + user="bob", + state={"test": "data"}, + ) + + # Multiple roundtrips + proto1 = original.model_dump_pb() + restored1 = GlobalContext.model_validate_pb(proto1) + proto2 = restored1.model_dump_pb() + restored2 = GlobalContext.model_validate_pb(proto2) + + assert original == restored1 == restored2 + + +class TestPluginViolationConversion: + """Test PluginViolation Pydantic <-> Protobuf conversion.""" + + def test_plugin_violation_basic_conversion(self): + """Test basic PluginViolation conversion.""" + violation = PluginViolation( + reason="Invalid input", + description="The input contains prohibited content", + code="PROHIBITED_CONTENT", + ) + + # Convert to protobuf + proto_violation = violation.model_dump_pb() + + # Verify protobuf fields + assert proto_violation.reason == "Invalid input" + assert proto_violation.description == "The input contains prohibited content" + assert proto_violation.code == "PROHIBITED_CONTENT" + + # Convert back to Pydantic + restored = PluginViolation.model_validate_pb(proto_violation) + + assert restored.reason == violation.reason + assert restored.description == violation.description + assert restored.code == violation.code + assert restored == violation + + def test_plugin_violation_with_details(self): + """Test PluginViolation with complex details dict.""" + violation = PluginViolation( + reason="Schema validation failed", + description="Multiple fields failed validation", + code="VALIDATION_ERROR", + details={ + "field": "email", + "error": "Invalid format", + "nested": {"key": "value"}, + }, + ) + + # Convert to protobuf + proto_violation = violation.model_dump_pb() + + # Convert back to Pydantic + restored = PluginViolation.model_validate_pb(proto_violation) + + assert restored.reason == violation.reason + assert restored.details["field"] == "email" + assert restored.details["error"] == "Invalid format" + assert "nested" in restored.details + + def test_plugin_violation_with_plugin_name(self): + """Test PluginViolation preserves plugin_name private attribute.""" + violation = PluginViolation( + reason="Test", + description="Test violation", + code="TEST", + ) + violation.plugin_name = "test_plugin" + + # Convert to protobuf + proto_violation = violation.model_dump_pb() + + # Verify plugin_name in proto + assert proto_violation.plugin_name == "test_plugin" + + # Convert back to Pydantic + restored = PluginViolation.model_validate_pb(proto_violation) + + assert restored.plugin_name == "test_plugin" + + def test_plugin_violation_empty_details(self): + """Test PluginViolation with empty details.""" + violation = PluginViolation( + reason="Test", + description="Test", + code="TEST", + details={}, + ) + + proto_violation = violation.model_dump_pb() + restored = PluginViolation.model_validate_pb(proto_violation) + + assert restored.details == {} + + +class TestPluginResultConversion: + """Test PluginResult Pydantic <-> Protobuf conversion.""" + + def test_plugin_result_basic_conversion(self): + """Test basic PluginResult conversion.""" + result: PluginResult[Any] = PluginResult( + continue_processing=True, + metadata={"key": "value"}, + ) + + # Convert to protobuf + proto_result = result.model_dump_pb() + + # Verify protobuf fields + assert proto_result.continue_processing is True + + # Convert back to Pydantic + restored = PluginResult.model_validate_pb(proto_result) + + assert restored.continue_processing == result.continue_processing + assert restored.metadata == result.metadata + + def test_plugin_result_with_violation(self): + """Test PluginResult with nested PluginViolation.""" + violation = PluginViolation( + reason="Access denied", + description="User lacks permission", + code="ACCESS_DENIED", + ) + result: PluginResult[Any] = PluginResult( + continue_processing=False, + violation=violation, + ) + + # Convert to protobuf + proto_result = result.model_dump_pb() + + # Verify nested violation + assert proto_result.HasField("violation") + assert proto_result.violation.reason == "Access denied" + + # Convert back to Pydantic + restored = PluginResult.model_validate_pb(proto_result) + + assert restored.continue_processing is False + assert restored.violation is not None + assert restored.violation.reason == "Access denied" + assert restored.violation.code == "ACCESS_DENIED" + + def test_plugin_result_continue_false(self): + """Test PluginResult with continue_processing=False.""" + result: PluginResult[Any] = PluginResult(continue_processing=False) + + proto_result = result.model_dump_pb() + restored = PluginResult.model_validate_pb(proto_result) + + assert restored.continue_processing is False + + def test_plugin_result_with_metadata(self): + """Test PluginResult with metadata dict.""" + result: PluginResult[Any] = PluginResult( + metadata={"plugin": "test", "duration_ms": "100"}, + ) + + proto_result = result.model_dump_pb() + restored = PluginResult.model_validate_pb(proto_result) + + assert restored.metadata["plugin"] == "test" + assert restored.metadata["duration_ms"] == "100" + + +class TestPluginContextConversion: + """Test PluginContext Pydantic <-> Protobuf conversion.""" + + def test_plugin_context_basic_conversion(self): + """Test basic PluginContext conversion.""" + global_ctx = GlobalContext(request_id="req-123") + ctx = PluginContext(global_context=global_ctx) + + # Convert to protobuf + proto_ctx = ctx.model_dump_pb() + + # Verify nested global_context + assert proto_ctx.global_context.request_id == "req-123" + + # Convert back to Pydantic + restored = PluginContext.model_validate_pb(proto_ctx) + + assert restored.global_context.request_id == "req-123" + assert restored.state == {} + assert restored.metadata == {} + + def test_plugin_context_with_state(self): + """Test PluginContext with state data.""" + global_ctx = GlobalContext(request_id="req-456") + ctx = PluginContext( + global_context=global_ctx, + state={ + "counter": 42, + "data": {"nested": "value"}, + }, + ) + + # Convert to protobuf + proto_ctx = ctx.model_dump_pb() + + # Convert back to Pydantic + restored = PluginContext.model_validate_pb(proto_ctx) + + assert "counter" in restored.state + assert "data" in restored.state + + def test_plugin_context_with_metadata(self): + """Test PluginContext with metadata.""" + global_ctx = GlobalContext(request_id="req-789") + ctx = PluginContext( + global_context=global_ctx, + metadata={"plugin_version": "1.0.0"}, + ) + + proto_ctx = ctx.model_dump_pb() + restored = PluginContext.model_validate_pb(proto_ctx) + + assert "plugin_version" in restored.metadata + + def test_plugin_context_complex(self): + """Test PluginContext with complex nested data.""" + global_ctx = GlobalContext( + request_id="req-complex", + user="alice", + state={"global_key": "global_value"}, + ) + ctx = PluginContext( + global_context=global_ctx, + state={ + "local_key": "local_value", + "nested": {"deep": {"key": "value"}}, + }, + metadata={"timestamp": "2024-01-01"}, + ) + + # Roundtrip conversion + proto_ctx = ctx.model_dump_pb() + restored = PluginContext.model_validate_pb(proto_ctx) + + assert restored.global_context.request_id == "req-complex" + assert restored.global_context.user == "alice" + assert "local_key" in restored.state + assert "timestamp" in restored.metadata + + +class TestConversionEdgeCases: + """Test edge cases and error conditions.""" + + def test_empty_global_context(self): + """Test conversion with minimal required fields.""" + ctx = GlobalContext(request_id="") + + proto_ctx = ctx.model_dump_pb() + restored = GlobalContext.model_validate_pb(proto_ctx) + + assert restored.request_id == "" + + def test_violation_with_empty_strings(self): + """Test PluginViolation with empty strings.""" + violation = PluginViolation(reason="", description="", code="") + + proto_violation = violation.model_dump_pb() + restored = PluginViolation.model_validate_pb(proto_violation) + + assert restored.reason == "" + assert restored.description == "" + assert restored.code == "" + + def test_plugin_result_defaults(self): + """Test PluginResult with all default values.""" + result: PluginResult[Any] = PluginResult() + + proto_result = result.model_dump_pb() + restored = PluginResult.model_validate_pb(proto_result) + + assert restored.continue_processing is True + assert restored.modified_payload is None + assert restored.violation is None + assert restored.metadata == {} + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/unit/mcpgateway/plugins/framework/generated/test_resources_protobuf_conversions.py b/tests/unit/mcpgateway/plugins/framework/generated/test_resources_protobuf_conversions.py new file mode 100644 index 000000000..54bb16641 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/generated/test_resources_protobuf_conversions.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +"""Tests for Resource hook Pydantic to Protobuf conversions. + +This module tests the model_dump_pb() and model_validate_pb() methods +for resource hook payload classes. +""" + +# Third-Party +import pytest + +# First-Party +from mcpgateway.common.models import ResourceContent +from mcpgateway.plugins.framework.hooks.resources import ( + ResourcePostFetchPayload, + ResourcePreFetchPayload, +) + +# Check if protobuf is available +try: + import google.protobuf # noqa: F401 + + PROTOBUF_AVAILABLE = True +except ImportError: + PROTOBUF_AVAILABLE = False + +pytestmark = pytest.mark.skipif(not PROTOBUF_AVAILABLE, reason="protobuf not installed") + + +class TestResourcePreFetchPayloadConversion: + """Test ResourcePreFetchPayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion(self): + """Test basic ResourcePreFetchPayload conversion to protobuf and back.""" + payload = ResourcePreFetchPayload(uri="file:///data.txt") + + # Convert to protobuf + proto_payload = payload.model_dump_pb() + + # Verify protobuf fields + assert proto_payload.uri == "file:///data.txt" + + # Convert back to Pydantic + restored = ResourcePreFetchPayload.model_validate_pb(proto_payload) + + # Verify restoration + assert restored.uri == payload.uri + assert restored.metadata == {} + + def test_with_metadata(self): + """Test ResourcePreFetchPayload with metadata.""" + payload = ResourcePreFetchPayload( + uri="http://api/data", + metadata={"Accept": "application/json", "version": "1.0"}, + ) + + proto_payload = payload.model_dump_pb() + restored = ResourcePreFetchPayload.model_validate_pb(proto_payload) + + assert restored.uri == "http://api/data" + assert restored.metadata["Accept"] == "application/json" + assert restored.metadata["version"] == "1.0" + + def test_with_nested_metadata(self): + """Test ResourcePreFetchPayload with nested metadata.""" + payload = ResourcePreFetchPayload( + uri="file:///docs/readme.md", + metadata={ + "version": "1.0", + "auth": {"type": "bearer", "token": "abc123"}, + "cache": {"ttl": 3600, "enabled": True}, + }, + ) + + proto_payload = payload.model_dump_pb() + restored = ResourcePreFetchPayload.model_validate_pb(proto_payload) + + assert restored.uri == "file:///docs/readme.md" + assert "auth" in restored.metadata + assert "cache" in restored.metadata + + def test_with_various_uri_schemes(self): + """Test ResourcePreFetchPayload with various URI schemes.""" + uris = [ + "file:///path/to/file.txt", + "http://example.com/resource", + "https://api.example.com/v1/data", + "s3://bucket/key", + "custom://resource/path", + ] + + for uri in uris: + payload = ResourcePreFetchPayload(uri=uri) + proto_payload = payload.model_dump_pb() + restored = ResourcePreFetchPayload.model_validate_pb(proto_payload) + assert restored.uri == uri + + def test_roundtrip_conversion(self): + """Test that multiple roundtrips maintain data integrity.""" + original = ResourcePreFetchPayload( + uri="test://resource", + metadata={"key": "value", "count": 42}, + ) + + proto1 = original.model_dump_pb() + restored1 = ResourcePreFetchPayload.model_validate_pb(proto1) + proto2 = restored1.model_dump_pb() + restored2 = ResourcePreFetchPayload.model_validate_pb(proto2) + + assert original.uri == restored2.uri + assert "key" in restored2.metadata + + +class TestResourcePostFetchPayloadConversion: + """Test ResourcePostFetchPayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion_with_resource_content(self): + """Test basic ResourcePostFetchPayload with ResourceContent.""" + content = ResourceContent( + type="resource", + id="res-1", + uri="file:///data.txt", + text="Hello World", + ) + payload = ResourcePostFetchPayload(uri="file:///data.txt", content=content) + + proto_payload = payload.model_dump_pb() + assert proto_payload.uri == "file:///data.txt" + + restored = ResourcePostFetchPayload.model_validate_pb(proto_payload) + assert restored.uri == "file:///data.txt" + assert restored.content["text"] == "Hello World" + assert restored.content["type"] == "resource" + + def test_with_dict_content(self): + """Test ResourcePostFetchPayload with dict content.""" + content = {"data": "test data", "size": 1024, "encoding": "utf-8"} + payload = ResourcePostFetchPayload(uri="test://resource", content=content) + + proto_payload = payload.model_dump_pb() + restored = ResourcePostFetchPayload.model_validate_pb(proto_payload) + + assert restored.content["data"] == "test data" + assert restored.content["size"] == 1024 + assert restored.content["encoding"] == "utf-8" + + def test_with_string_content(self): + """Test ResourcePostFetchPayload with string content.""" + payload = ResourcePostFetchPayload(uri="file:///text.txt", content="Plain text content") + + proto_payload = payload.model_dump_pb() + restored = ResourcePostFetchPayload.model_validate_pb(proto_payload) + + # String content is wrapped in "value" key + assert restored.content == "Plain text content" + + def test_with_binary_like_content(self): + """Test ResourcePostFetchPayload with binary-like content.""" + content = ResourceContent( + type="resource", + id="res-binary", + uri="file:///image.png", + blob="base64encodeddata", + mime_type="image/png", + ) + payload = ResourcePostFetchPayload(uri="file:///image.png", content=content) + + proto_payload = payload.model_dump_pb() + restored = ResourcePostFetchPayload.model_validate_pb(proto_payload) + + assert restored.content["blob"] == "base64encodeddata" + # Note: mime_type field name is preserved + assert restored.content["mime_type"] == "image/png" + + def test_with_nested_content_structure(self): + """Test ResourcePostFetchPayload with nested content.""" + content = { + "metadata": {"author": "Alice", "created": "2024-01-01"}, + "data": {"sections": [{"title": "Intro", "content": "..."}]}, + } + payload = ResourcePostFetchPayload(uri="doc://complex", content=content) + + proto_payload = payload.model_dump_pb() + restored = ResourcePostFetchPayload.model_validate_pb(proto_payload) + + assert "metadata" in restored.content + assert "data" in restored.content + + def test_with_none_content(self): + """Test ResourcePostFetchPayload with None content.""" + payload = ResourcePostFetchPayload(uri="empty://resource", content=None) + + proto_payload = payload.model_dump_pb() + restored = ResourcePostFetchPayload.model_validate_pb(proto_payload) + + assert restored.uri == "empty://resource" + # Note: protobuf Struct converts None to empty dict {} + assert restored.content == {} or restored.content is None + + def test_roundtrip_conversion(self): + """Test that multiple roundtrips maintain data integrity.""" + content = ResourceContent( + type="resource", + id="res-roundtrip", + uri="test://data", + text="Test content", + ) + original = ResourcePostFetchPayload(uri="test://data", content=content) + + proto1 = original.model_dump_pb() + restored1 = ResourcePostFetchPayload.model_validate_pb(proto1) + proto2 = restored1.model_dump_pb() + restored2 = ResourcePostFetchPayload.model_validate_pb(proto2) + + assert original.uri == restored2.uri + assert restored2.content["text"] == "Test content" + + +class TestResourcePayloadEdgeCases: + """Test edge cases for resource payload conversions.""" + + def test_empty_uri(self): + """Test with empty URI.""" + payload = ResourcePreFetchPayload(uri="") + + proto_payload = payload.model_dump_pb() + restored = ResourcePreFetchPayload.model_validate_pb(proto_payload) + + assert restored.uri == "" + + def test_very_long_uri(self): + """Test with very long URI.""" + long_uri = "http://example.com/" + "a" * 1000 + payload = ResourcePreFetchPayload(uri=long_uri) + + proto_payload = payload.model_dump_pb() + restored = ResourcePreFetchPayload.model_validate_pb(proto_payload) + + assert restored.uri == long_uri + + def test_uri_with_special_characters(self): + """Test URI with special characters.""" + uri = "file:///path/with spaces/and-special_chars#fragment?query=value" + payload = ResourcePreFetchPayload(uri=uri) + + proto_payload = payload.model_dump_pb() + restored = ResourcePreFetchPayload.model_validate_pb(proto_payload) + + assert restored.uri == uri + + def test_large_metadata_dict(self): + """Test with large metadata dictionary.""" + large_metadata = {f"meta_{i}": f"value_{i}" for i in range(100)} + payload = ResourcePreFetchPayload(uri="test://bulk", metadata=large_metadata) + + proto_payload = payload.model_dump_pb() + restored = ResourcePreFetchPayload.model_validate_pb(proto_payload) + + assert len(restored.metadata) == 100 + assert restored.metadata["meta_50"] == "value_50" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/unit/mcpgateway/plugins/framework/generated/test_tools_protobuf_conversions.py b/tests/unit/mcpgateway/plugins/framework/generated/test_tools_protobuf_conversions.py new file mode 100644 index 000000000..a4242b2a5 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/generated/test_tools_protobuf_conversions.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +"""Tests for Tool hook Pydantic to Protobuf conversions. + +This module tests the model_dump_pb() and model_validate_pb() methods +for tool hook payload classes. +""" + +# Third-Party +import pytest + +# First-Party +from mcpgateway.plugins.framework.hooks.tools import ( + ToolPostInvokePayload, + ToolPreInvokePayload, +) + +# Check if protobuf is available +try: + import google.protobuf # noqa: F401 + + PROTOBUF_AVAILABLE = True +except ImportError: + PROTOBUF_AVAILABLE = False + +pytestmark = pytest.mark.skipif(not PROTOBUF_AVAILABLE, reason="protobuf not installed") + + +class TestToolPreInvokePayloadConversion: + """Test ToolPreInvokePayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion(self): + """Test basic ToolPreInvokePayload conversion to protobuf and back.""" + payload = ToolPreInvokePayload( + name="test_tool", + args={"input": "data", "count": 42}, + ) + + # Convert to protobuf + proto_payload = payload.model_dump_pb() + + # Verify protobuf fields + assert proto_payload.name == "test_tool" + + # Convert back to Pydantic + restored = ToolPreInvokePayload.model_validate_pb(proto_payload) + + # Verify restoration + assert restored.name == payload.name + assert restored.args == payload.args + assert restored == payload + + def test_with_empty_args(self): + """Test ToolPreInvokePayload with empty args.""" + payload = ToolPreInvokePayload(name="empty_tool") + + proto_payload = payload.model_dump_pb() + restored = ToolPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.name == "empty_tool" + assert restored.args == {} + + def test_with_headers(self): + """Test ToolPreInvokePayload with HTTP headers.""" + from mcpgateway.plugins.framework.hooks.http import HttpHeaderPayload + + headers = HttpHeaderPayload({"Authorization": "Bearer token123", "Content-Type": "application/json"}) + payload = ToolPreInvokePayload( + name="api_tool", + args={"query": "test"}, + headers=headers, + ) + + proto_payload = payload.model_dump_pb() + restored = ToolPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.name == "api_tool" + assert restored.args["query"] == "test" + assert restored.headers["Authorization"] == "Bearer token123" + assert restored.headers["Content-Type"] == "application/json" + + def test_with_nested_args(self): + """Test ToolPreInvokePayload with nested argument structures.""" + payload = ToolPreInvokePayload( + name="complex_tool", + args={ + "operation": "calculate", + "params": {"a": 5, "b": 10, "operation": "add"}, + "metadata": {"version": "1.0"}, + }, + ) + + proto_payload = payload.model_dump_pb() + restored = ToolPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.name == "complex_tool" + assert restored.args["operation"] == "calculate" + assert "params" in restored.args + assert "metadata" in restored.args + + def test_roundtrip_conversion(self): + """Test that multiple roundtrips maintain data integrity.""" + from mcpgateway.plugins.framework.hooks.http import HttpHeaderPayload + + headers = HttpHeaderPayload({"X-Custom": "value"}) + original = ToolPreInvokePayload( + name="roundtrip_tool", + args={"data": "test", "count": 3}, + headers=headers, + ) + + # Multiple roundtrips + proto1 = original.model_dump_pb() + restored1 = ToolPreInvokePayload.model_validate_pb(proto1) + proto2 = restored1.model_dump_pb() + restored2 = ToolPreInvokePayload.model_validate_pb(proto2) + + assert original.name == restored2.name + assert original.args == restored2.args + assert restored2.headers["X-Custom"] == "value" + + +class TestToolPostInvokePayloadConversion: + """Test ToolPostInvokePayload Pydantic <-> Protobuf conversion.""" + + def test_basic_conversion_with_dict_result(self): + """Test basic ToolPostInvokePayload with dict result.""" + payload = ToolPostInvokePayload( + name="calculator", + result={"result": 42, "status": "success"}, + ) + + proto_payload = payload.model_dump_pb() + assert proto_payload.name == "calculator" + + restored = ToolPostInvokePayload.model_validate_pb(proto_payload) + assert restored.name == "calculator" + assert restored.result["result"] == 42 + assert restored.result["status"] == "success" + + def test_with_string_result(self): + """Test ToolPostInvokePayload with string result.""" + payload = ToolPostInvokePayload( + name="text_tool", + result="Hello World", + ) + + proto_payload = payload.model_dump_pb() + restored = ToolPostInvokePayload.model_validate_pb(proto_payload) + + # String results are wrapped in "value" key during conversion + assert restored.result == "Hello World" + + def test_with_numeric_result(self): + """Test ToolPostInvokePayload with numeric result.""" + payload = ToolPostInvokePayload( + name="math_tool", + result=123.45, + ) + + proto_payload = payload.model_dump_pb() + restored = ToolPostInvokePayload.model_validate_pb(proto_payload) + + assert restored.result == 123.45 + + def test_with_complex_nested_result(self): + """Test ToolPostInvokePayload with complex nested result.""" + payload = ToolPostInvokePayload( + name="analytics_tool", + result={ + "summary": {"total": 100, "processed": 95}, + "details": [{"id": 1, "status": "ok"}, {"id": 2, "status": "ok"}], + "metadata": {"timestamp": "2024-01-01T00:00:00Z"}, + }, + ) + + proto_payload = payload.model_dump_pb() + restored = ToolPostInvokePayload.model_validate_pb(proto_payload) + + assert restored.name == "analytics_tool" + assert "summary" in restored.result + assert "details" in restored.result + assert "metadata" in restored.result + + def test_with_none_result(self): + """Test ToolPostInvokePayload with None result.""" + payload = ToolPostInvokePayload( + name="void_tool", + result=None, + ) + + proto_payload = payload.model_dump_pb() + restored = ToolPostInvokePayload.model_validate_pb(proto_payload) + + assert restored.name == "void_tool" + # Note: protobuf Struct converts None to empty dict {} + assert restored.result == {} or restored.result is None + + def test_roundtrip_conversion(self): + """Test that multiple roundtrips maintain data integrity.""" + original = ToolPostInvokePayload( + name="data_tool", + result={"key1": "value1", "key2": 123, "key3": [1, 2, 3]}, + ) + + proto1 = original.model_dump_pb() + restored1 = ToolPostInvokePayload.model_validate_pb(proto1) + proto2 = restored1.model_dump_pb() + restored2 = ToolPostInvokePayload.model_validate_pb(proto2) + + assert original.name == restored2.name + # Dict comparison for complex results + assert restored2.result["key1"] == "value1" + assert restored2.result["key2"] == 123 + + +class TestToolPayloadEdgeCases: + """Test edge cases for tool payload conversions.""" + + def test_tool_name_with_special_characters(self): + """Test tool names with special characters.""" + payload = ToolPreInvokePayload( + name="my-tool_v2.0", + args={"test": "data"}, + ) + + proto_payload = payload.model_dump_pb() + restored = ToolPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.name == "my-tool_v2.0" + + def test_empty_tool_name(self): + """Test with empty tool name.""" + payload = ToolPreInvokePayload(name="", args={}) + + proto_payload = payload.model_dump_pb() + restored = ToolPreInvokePayload.model_validate_pb(proto_payload) + + assert restored.name == "" + + def test_large_args_dict(self): + """Test with large arguments dictionary.""" + large_args = {f"key_{i}": f"value_{i}" for i in range(100)} + payload = ToolPreInvokePayload(name="bulk_tool", args=large_args) + + proto_payload = payload.model_dump_pb() + restored = ToolPreInvokePayload.model_validate_pb(proto_payload) + + assert len(restored.args) == 100 + assert restored.args["key_50"] == "value_50" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])