From 8e17ec94a25e32f0df373351bea87ce4b9b463e7 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Tue, 10 Feb 2026 18:45:30 +0000 Subject: [PATCH 01/13] nemocheck internal plugin Signed-off-by: julianstephen --- plugins/examples/nemocheck-internal/README.md | 10 ++ .../examples/nemocheck-internal/__init__.py | 7 + .../examples/nemocheck-internal/config.yaml | 28 ++++ .../nemocheck-internal/plugin-manifest.yaml | 9 ++ plugins/examples/nemocheck-internal/plugin.py | 138 ++++++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 plugins/examples/nemocheck-internal/README.md create mode 100644 plugins/examples/nemocheck-internal/__init__.py create mode 100644 plugins/examples/nemocheck-internal/config.yaml create mode 100644 plugins/examples/nemocheck-internal/plugin-manifest.yaml create mode 100644 plugins/examples/nemocheck-internal/plugin.py diff --git a/plugins/examples/nemocheck-internal/README.md b/plugins/examples/nemocheck-internal/README.md new file mode 100644 index 0000000..9eab97f --- /dev/null +++ b/plugins/examples/nemocheck-internal/README.md @@ -0,0 +1,10 @@ +# NemoCheckv2 for Context Forge MCP Gateway + +Nemo Check Adapter. + + +## Installation + +1. Copy .env.example .env +2. Enable plugins in `.env` +3. Add the plugin configuration to `plugins/config.yaml`: diff --git a/plugins/examples/nemocheck-internal/__init__.py b/plugins/examples/nemocheck-internal/__init__.py new file mode 100644 index 0000000..16e5be5 --- /dev/null +++ b/plugins/examples/nemocheck-internal/__init__.py @@ -0,0 +1,7 @@ +"""MCP Gateway NemoCheckv2 Plugin - Nemo Check Adapter. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: julianstephen + +""" diff --git a/plugins/examples/nemocheck-internal/config.yaml b/plugins/examples/nemocheck-internal/config.yaml new file mode 100644 index 0000000..c8c96a9 --- /dev/null +++ b/plugins/examples/nemocheck-internal/config.yaml @@ -0,0 +1,28 @@ +plugins: + - name: "NemoCheckv2" + kind: "nemocheckv2.plugin.NemoCheckv2" + description: "Nemo Check Adapter" + version: "0.1.0" + author: "julianstephen" + hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke"] + tags: ["plugin"] + mode: "enforce" # enforce | permissive | disabled + priority: 150 + conditions: + # Apply to specific tools/servers + - server_ids: [] # Apply to all servers + tenant_ids: [] # Apply to all tenants + config: + # Plugin config dict passed to the plugin constructor + +# Plugin directories to scan +plugin_dirs: + - "nemocheckv2" + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/plugins/examples/nemocheck-internal/plugin-manifest.yaml b/plugins/examples/nemocheck-internal/plugin-manifest.yaml new file mode 100644 index 0000000..7306468 --- /dev/null +++ b/plugins/examples/nemocheck-internal/plugin-manifest.yaml @@ -0,0 +1,9 @@ +description: "Nemo Check Adapter" +author: "julianstephen" +version: "0.1.0" +available_hooks: + - "prompt_pre_hook" + - "prompt_post_hook" + - "tool_pre_hook" + - "tool_post_hook" +default_configs: diff --git a/plugins/examples/nemocheck-internal/plugin.py b/plugins/examples/nemocheck-internal/plugin.py new file mode 100644 index 0000000..4211ea1 --- /dev/null +++ b/plugins/examples/nemocheck-internal/plugin.py @@ -0,0 +1,138 @@ +"""Nemo Check Adapter. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: julianstephen + +This module loads configurations for plugins. +""" + +# First-Party +from mcpgateway.plugins.framework import ( + Plugin, + PluginConfig, + PluginContext, + PromptPosthookPayload, + PromptPosthookResult, + PromptPrehookPayload, + PromptPrehookResult, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, + ToolPreInvokeResult, +) + +import logging +import os + +# Initialize logging service first +logger = logging.getLogger(__name__) +log_level = os.getenv("LOGLEVEL", "INFO").upper() +logger.setLevel(log_level) + +MODEL_NAME = os.getenv("NEMO_MODEL", "meta-llama/llama-3-3-70b-instruct") # Currently only for logging. +CHECK_ENDPOINT = os.getenv("CHECK_ENDPOINT", "http://nemo-guardrails-service:8000") + +class NemoCheckv2(Plugin): + """Nemo Check Adapter.""" + + def __init__(self, config: PluginConfig): + """Entry init block for plugin. + + Args: + logger: logger that the skill can make use of + config: the skill configuration + """ + super().__init__(config) + + async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + """The plugin hook run before a prompt is retrieved and rendered. + + Args: + payload: The prompt payload to be analyzed. + context: contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the prompt can proceed. + """ + return PromptPrehookResult(continue_processing=True) + + async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: + """Plugin hook run after a prompt is rendered. + + Args: + payload: The prompt payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the prompt can proceed. + """ + return PromptPosthookResult(continue_processing=True) + + async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + """Plugin hook run before a tool is invoked. + + Args: + payload: The tool payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the tool can proceed. + """ + logger.info("tool_pre_invoke....") + logger.info(payload) + tool_name = payload.name # ("tool_name", None) + check_nemo_payload = { + "model": MODEL_NAME, + "messages": [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_plug_adap_nem_check_123", + "type": "function", + "function": { + "name": tool_name, + "arguments": payload.args.get("tool_args", None), + }, + } + ], + } + ], + } + violation = None + response = requests.post(CHECK_ENDPOINT, headers=headers, json=check_nemo_payload) + if response.status_code == 200: + data = response.json() + status = data.get("status", "blocked") + logger.debug(f"rails reply:{data}") + if status == "success": + metadata = data.get("rails_status") + result = ToolPreInvokeResult(continue_processing=True, metadata=metadata) + else: + metadata = data.get("rails_status") + violation = PluginViolation( + reason=f"Tool Check status:{status}", description="Rails check blocked request", code=f"checkserver_http_status_code:{response.status_code}", details=metadata + ) + result = ToolPreInvokeResult(continue_processing=False, violation=violation, metadata=metadata) + + else: + violation = PluginViolation( + reason="Tool Check Unavailable", description="Tool arguments check server returned error:", code=f"checkserver_http_status_code:{response.status_code}", details={} + ) + result = ToolPreInvokeResult(continue_processing=False, violation=violation) + logger.info(response) + + return ToolPreInvokeResult(continue_processing=True) + + async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: + """Plugin hook run after a tool is invoked. + + Args: + payload: The tool result payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the tool result should proceed. + """ + return ToolPostInvokeResult(continue_processing=True) From 7538019b51c4f2ec2fae1247e5a758be79a24ad0 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Tue, 10 Feb 2026 21:40:00 +0000 Subject: [PATCH 02/13] renamed package name not to have dashes Signed-off-by: julianstephen --- .../README.md | 0 .../__init__.py | 0 .../config.yaml | 15 ++++++++- .../plugin-manifest.yaml | 0 .../plugin.py | 0 resources/config/config.yaml | 32 +++++++------------ 6 files changed, 26 insertions(+), 21 deletions(-) rename plugins/examples/{nemocheck-internal => nemocheckinternal}/README.md (100%) rename plugins/examples/{nemocheck-internal => nemocheckinternal}/__init__.py (100%) rename plugins/examples/{nemocheck-internal => nemocheckinternal}/config.yaml (62%) rename plugins/examples/{nemocheck-internal => nemocheckinternal}/plugin-manifest.yaml (100%) rename plugins/examples/{nemocheck-internal => nemocheckinternal}/plugin.py (100%) diff --git a/plugins/examples/nemocheck-internal/README.md b/plugins/examples/nemocheckinternal/README.md similarity index 100% rename from plugins/examples/nemocheck-internal/README.md rename to plugins/examples/nemocheckinternal/README.md diff --git a/plugins/examples/nemocheck-internal/__init__.py b/plugins/examples/nemocheckinternal/__init__.py similarity index 100% rename from plugins/examples/nemocheck-internal/__init__.py rename to plugins/examples/nemocheckinternal/__init__.py diff --git a/plugins/examples/nemocheck-internal/config.yaml b/plugins/examples/nemocheckinternal/config.yaml similarity index 62% rename from plugins/examples/nemocheck-internal/config.yaml rename to plugins/examples/nemocheckinternal/config.yaml index c8c96a9..b01f3c4 100644 --- a/plugins/examples/nemocheck-internal/config.yaml +++ b/plugins/examples/nemocheckinternal/config.yaml @@ -1,6 +1,19 @@ plugins: + # Nemo example + - name: "NemoWrapperPlugin" + kind: "plugins.examples.nemo.nemo_wrapper_plugin.NemoWrapperPlugin" + description: "A simple Nemo PII detector" + version: "0.1.0" + author: "Evaline Ju" + hooks: ["tool_pre_invoke", "tool_post_invoke"] + tags: ["plugin", "pre-post"] + mode: "enforce" # enforce | permissive | disabled + priority: 150 + config: + foo: bar + - name: "NemoCheckv2" - kind: "nemocheckv2.plugin.NemoCheckv2" + kind: "plugins.examples.nemocheck-internal.nemocheckv2.plugin.NemoCheckv2" description: "Nemo Check Adapter" version: "0.1.0" author: "julianstephen" diff --git a/plugins/examples/nemocheck-internal/plugin-manifest.yaml b/plugins/examples/nemocheckinternal/plugin-manifest.yaml similarity index 100% rename from plugins/examples/nemocheck-internal/plugin-manifest.yaml rename to plugins/examples/nemocheckinternal/plugin-manifest.yaml diff --git a/plugins/examples/nemocheck-internal/plugin.py b/plugins/examples/nemocheckinternal/plugin.py similarity index 100% rename from plugins/examples/nemocheck-internal/plugin.py rename to plugins/examples/nemocheckinternal/plugin.py diff --git a/resources/config/config.yaml b/resources/config/config.yaml index 812a5e7..3076913 100644 --- a/resources/config/config.yaml +++ b/resources/config/config.yaml @@ -1,26 +1,18 @@ # plugins/config.yaml - Main plugin configuration file plugins: + - name: NemoCheck + kind: external + hooks: ["tool_pre_invoke", "tool_post_invoke"] + mode: enforce + mcp: + proto: STREAMABLEHTTP + url: http://nemocheck-plugin-service:8000/mcp +# tls: +# certfile: /path/to/client-cert.pem +# keyfile: /path/to/client-key.pem +# ca_bundle: /path/to/ca-bundle.pem +# verify: false # Self-contained Search Replace Plugin - - name: "ReplaceBadWordsPlugin" - kind: "plugins.regex_filter.search_replace.SearchReplacePlugin" - description: "A plugin for finding and replacing words." - version: "0.1.0" - author: "Teryl Taylor" - hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke"] - tags: ["plugin", "transformer", "regex", "search-and-replace", "pre-post"] - mode: "enforce" # enforce | permissive | disabled - priority: 150 - conditions: - # Apply to specific tools/servers - - prompts: ["test_prompt"] - server_ids: [] # Apply to all servers - tenant_ids: [] # Apply to all tenants - config: - words: - - search: crap - replace: crud - - search: crud - replace: yikes # Plugin directories to scan plugin_dirs: From 75628564d05682a33bebbd6587e984a7e5ccb681 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Tue, 10 Feb 2026 22:12:46 +0000 Subject: [PATCH 03/13] updated plugin config to read checkserver url Signed-off-by: julianstephen --- .../examples/nemocheckinternal/config.yaml | 13 ----- plugins/examples/nemocheckinternal/plugin.py | 5 ++ resources/config/config.yaml | 55 +++++++++++++------ 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/plugins/examples/nemocheckinternal/config.yaml b/plugins/examples/nemocheckinternal/config.yaml index b01f3c4..191c556 100644 --- a/plugins/examples/nemocheckinternal/config.yaml +++ b/plugins/examples/nemocheckinternal/config.yaml @@ -1,17 +1,4 @@ plugins: - # Nemo example - - name: "NemoWrapperPlugin" - kind: "plugins.examples.nemo.nemo_wrapper_plugin.NemoWrapperPlugin" - description: "A simple Nemo PII detector" - version: "0.1.0" - author: "Evaline Ju" - hooks: ["tool_pre_invoke", "tool_post_invoke"] - tags: ["plugin", "pre-post"] - mode: "enforce" # enforce | permissive | disabled - priority: 150 - config: - foo: bar - - name: "NemoCheckv2" kind: "plugins.examples.nemocheck-internal.nemocheckv2.plugin.NemoCheckv2" description: "Nemo Check Adapter" diff --git a/plugins/examples/nemocheckinternal/plugin.py b/plugins/examples/nemocheckinternal/plugin.py index 4211ea1..8eae87a 100644 --- a/plugins/examples/nemocheckinternal/plugin.py +++ b/plugins/examples/nemocheckinternal/plugin.py @@ -43,6 +43,11 @@ def __init__(self, config: PluginConfig): logger: logger that the skill can make use of config: the skill configuration """ + global CHECK_ENDPOINT + CHECK_ENDPOINT = config.get('checkserver_url',None) + if CHECK_ENDPOINT is None: + CHECK_ENDPOINT = os.getenv("CHECK_ENDPOINT", "http://nemo-guardrails-service:8000") + logger.info(f"checkserver at {config}:{CHECK_ENDPOINT}") super().__init__(config) async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: diff --git a/resources/config/config.yaml b/resources/config/config.yaml index 3076913..39713e7 100644 --- a/resources/config/config.yaml +++ b/resources/config/config.yaml @@ -1,24 +1,47 @@ # plugins/config.yaml - Main plugin configuration file plugins: - - name: NemoCheck - kind: external - hooks: ["tool_pre_invoke", "tool_post_invoke"] - mode: enforce - mcp: - proto: STREAMABLEHTTP - url: http://nemocheck-plugin-service:8000/mcp -# tls: -# certfile: /path/to/client-cert.pem -# keyfile: /path/to/client-key.pem -# ca_bundle: /path/to/ca-bundle.pem -# verify: false -# Self-contained Search Replace Plugin + # Self-contained Search Replace Plugin - depends on plugin availability + - name: "ReplaceBadWordsPlugin" + # From https://github.com/contextforge-org/contextforge-plugins-python + kind: "contextforge-plugins-python.regex_filter.search_replace.SearchReplacePlugin" + description: "A plugin for finding and replacing words." + version: "0.1.0" + author: "Teryl Taylor" + hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke"] + tags: ["plugin", "transformer", "regex", "search-and-replace", "pre-post"] + mode: "enforce" # enforce | permissive | disabled + priority: 150 + conditions: + # Apply to specific tools/servers + - prompts: ["test_prompt"] + server_ids: [] # Apply to all servers + tenant_ids: [] # Apply to all tenants + config: + words: + - search: crap + replace: crud + - search: crud + replace: yikes + # Nemo Check Example + - name: "NemoCheckv2" + kind: "plugins.examples.nemocheckinternal.plugin.NemoCheckv2" + description: "Adapter for nemo check server" + version: "0.1.0" + author: "Julian Stephen" + hooks: ["tool_pre_invoke", "tool_post_invoke"] + tags: ["plugin", "pre-post"] + mode: "enforce" # enforce | permissive | disabled + priority: 150 + config: + checkserver_url: "http://nemo-guardrails-service:8000/v1/guardrail/checks" # Plugin directories to scan plugin_dirs: - - "plugins/native" # Built-in plugins - - "plugins/custom" # Custom organization plugins + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins - "/etc/mcpgateway/plugins" # System-wide plugins + - "plugins/examples/nemo" # Example Nemo guardrails plugins + - "plugins/examples/nemocheckinternal" # Nemo Check Server plugins # Global plugin settings plugin_settings: @@ -26,4 +49,4 @@ plugin_settings: plugin_timeout: 30 fail_on_plugin_error: false enable_plugin_api: true - plugin_health_check_interval: 60 + plugin_health_check_interval: 60 \ No newline at end of file From 68482b963a076c07dd41f65732c52137fd73b7a8 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Tue, 10 Feb 2026 22:39:02 +0000 Subject: [PATCH 04/13] fix errors on plugin config init Signed-off-by: julianstephen --- plugins/examples/nemocheckinternal/config.yaml | 2 +- plugins/examples/nemocheckinternal/plugin-manifest.yaml | 1 + plugins/examples/nemocheckinternal/plugin.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/examples/nemocheckinternal/config.yaml b/plugins/examples/nemocheckinternal/config.yaml index 191c556..d930000 100644 --- a/plugins/examples/nemocheckinternal/config.yaml +++ b/plugins/examples/nemocheckinternal/config.yaml @@ -1,6 +1,6 @@ plugins: - name: "NemoCheckv2" - kind: "plugins.examples.nemocheck-internal.nemocheckv2.plugin.NemoCheckv2" + kind: "plugins.examples.nemocheckinternal.plugin.NemoCheckv2" description: "Nemo Check Adapter" version: "0.1.0" author: "julianstephen" diff --git a/plugins/examples/nemocheckinternal/plugin-manifest.yaml b/plugins/examples/nemocheckinternal/plugin-manifest.yaml index 7306468..d8fa01f 100644 --- a/plugins/examples/nemocheckinternal/plugin-manifest.yaml +++ b/plugins/examples/nemocheckinternal/plugin-manifest.yaml @@ -1,4 +1,5 @@ description: "Nemo Check Adapter" +name: NemoCheckv2 author: "julianstephen" version: "0.1.0" available_hooks: diff --git a/plugins/examples/nemocheckinternal/plugin.py b/plugins/examples/nemocheckinternal/plugin.py index 8eae87a..a93aa87 100644 --- a/plugins/examples/nemocheckinternal/plugin.py +++ b/plugins/examples/nemocheckinternal/plugin.py @@ -24,6 +24,7 @@ import logging import os +import requests # Initialize logging service first logger = logging.getLogger(__name__) @@ -44,7 +45,8 @@ def __init__(self, config: PluginConfig): config: the skill configuration """ global CHECK_ENDPOINT - CHECK_ENDPOINT = config.get('checkserver_url',None) + logger.info(f"plugin config {config}") + CHECK_ENDPOINT = config.config.get('checkserver_url',None) if CHECK_ENDPOINT is None: CHECK_ENDPOINT = os.getenv("CHECK_ENDPOINT", "http://nemo-guardrails-service:8000") logger.info(f"checkserver at {config}:{CHECK_ENDPOINT}") From 2dda22e8dd7912e3ea44be238eedaa4c80747914 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Tue, 10 Feb 2026 22:42:01 +0000 Subject: [PATCH 05/13] fix errors on plugin config init Signed-off-by: julianstephen --- plugins/examples/nemocheckinternal/plugin.py | 5 ++++ .../config/nemocheck-internal-config.yaml | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 resources/config/nemocheck-internal-config.yaml diff --git a/plugins/examples/nemocheckinternal/plugin.py b/plugins/examples/nemocheckinternal/plugin.py index a93aa87..55b1507 100644 --- a/plugins/examples/nemocheckinternal/plugin.py +++ b/plugins/examples/nemocheckinternal/plugin.py @@ -34,6 +34,11 @@ MODEL_NAME = os.getenv("NEMO_MODEL", "meta-llama/llama-3-3-70b-instruct") # Currently only for logging. CHECK_ENDPOINT = os.getenv("CHECK_ENDPOINT", "http://nemo-guardrails-service:8000") + +headers = { + "Content-Type": "application/json", +} + class NemoCheckv2(Plugin): """Nemo Check Adapter.""" diff --git a/resources/config/nemocheck-internal-config.yaml b/resources/config/nemocheck-internal-config.yaml new file mode 100644 index 0000000..8650cb1 --- /dev/null +++ b/resources/config/nemocheck-internal-config.yaml @@ -0,0 +1,30 @@ +# plugins/config.yaml - Main plugin configuration file +plugins: + # Nemo Check Example + - name: "NemoCheckv2" + kind: "plugins.examples.nemocheckinternal.plugin.NemoCheckv2" + description: "Adapter for nemo check server" + version: "0.1.0" + author: "Julian Stephen" + hooks: ["tool_pre_invoke", "tool_post_invoke"] + tags: ["plugin", "pre-post"] + mode: "enforce" # enforce | permissive | disabled + priority: 150 + config: + checkserver_url: "http://nemo-guardrails-service:8000/v1/guardrail/checks" + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + - "plugins/examples/nemo" # Example Nemo guardrails plugins + - "plugins/examples/nemocheckinternal" # Nemo Check Server plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 \ No newline at end of file From 43d2ad4f711cb2a275685b608832793cef6501de Mon Sep 17 00:00:00 2001 From: julianstephen Date: Tue, 10 Feb 2026 22:43:44 +0000 Subject: [PATCH 06/13] fix errors on plugin config init Signed-off-by: julianstephen --- plugins/examples/nemocheckinternal/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/examples/nemocheckinternal/plugin.py b/plugins/examples/nemocheckinternal/plugin.py index 55b1507..59f5670 100644 --- a/plugins/examples/nemocheckinternal/plugin.py +++ b/plugins/examples/nemocheckinternal/plugin.py @@ -20,6 +20,7 @@ ToolPostInvokeResult, ToolPreInvokePayload, ToolPreInvokeResult, + PluginViolation, ) import logging From d330823164ec6d0d2217f91a47a91eb1d61dceb2 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Wed, 11 Feb 2026 15:50:23 +0000 Subject: [PATCH 07/13] formatting fixes Signed-off-by: julianstephen --- plugins/examples/nemocheckinternal/plugin.py | 56 ++++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/plugins/examples/nemocheckinternal/plugin.py b/plugins/examples/nemocheckinternal/plugin.py index 59f5670..b0edb03 100644 --- a/plugins/examples/nemocheckinternal/plugin.py +++ b/plugins/examples/nemocheckinternal/plugin.py @@ -20,7 +20,7 @@ ToolPostInvokeResult, ToolPreInvokePayload, ToolPreInvokeResult, - PluginViolation, + PluginViolation, ) import logging @@ -32,7 +32,9 @@ log_level = os.getenv("LOGLEVEL", "INFO").upper() logger.setLevel(log_level) -MODEL_NAME = os.getenv("NEMO_MODEL", "meta-llama/llama-3-3-70b-instruct") # Currently only for logging. +MODEL_NAME = os.getenv( + "NEMO_MODEL", "meta-llama/llama-3-3-70b-instruct" +) # Currently only for logging. CHECK_ENDPOINT = os.getenv("CHECK_ENDPOINT", "http://nemo-guardrails-service:8000") @@ -40,6 +42,7 @@ "Content-Type": "application/json", } + class NemoCheckv2(Plugin): """Nemo Check Adapter.""" @@ -52,13 +55,17 @@ def __init__(self, config: PluginConfig): """ global CHECK_ENDPOINT logger.info(f"plugin config {config}") - CHECK_ENDPOINT = config.config.get('checkserver_url',None) + CHECK_ENDPOINT = config.config.get("checkserver_url", None) if CHECK_ENDPOINT is None: - CHECK_ENDPOINT = os.getenv("CHECK_ENDPOINT", "http://nemo-guardrails-service:8000") + CHECK_ENDPOINT = os.getenv( + "CHECK_ENDPOINT", "http://nemo-guardrails-service:8000" + ) logger.info(f"checkserver at {config}:{CHECK_ENDPOINT}") super().__init__(config) - async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + async def prompt_pre_fetch( + self, payload: PromptPrehookPayload, context: PluginContext + ) -> PromptPrehookResult: """The plugin hook run before a prompt is retrieved and rendered. Args: @@ -70,7 +77,9 @@ async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginC """ return PromptPrehookResult(continue_processing=True) - async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: + async def prompt_post_fetch( + self, payload: PromptPosthookPayload, context: PluginContext + ) -> PromptPosthookResult: """Plugin hook run after a prompt is rendered. Args: @@ -82,7 +91,9 @@ async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: Plugi """ return PromptPosthookResult(continue_processing=True) - async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + async def tool_pre_invoke( + self, payload: ToolPreInvokePayload, context: PluginContext + ) -> ToolPreInvokeResult: """Plugin hook run before a tool is invoked. Args: @@ -114,31 +125,44 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo ], } violation = None - response = requests.post(CHECK_ENDPOINT, headers=headers, json=check_nemo_payload) + response = requests.post( + CHECK_ENDPOINT, headers=headers, json=check_nemo_payload + ) if response.status_code == 200: data = response.json() status = data.get("status", "blocked") logger.debug(f"rails reply:{data}") if status == "success": metadata = data.get("rails_status") - result = ToolPreInvokeResult(continue_processing=True, metadata=metadata) + result = ToolPreInvokeResult( + continue_processing=True, metadata=metadata + ) else: metadata = data.get("rails_status") violation = PluginViolation( - reason=f"Tool Check status:{status}", description="Rails check blocked request", code=f"checkserver_http_status_code:{response.status_code}", details=metadata + reason=f"Tool Check status:{status}", + description="Rails check blocked request", + code=f"checkserver_http_status_code:{response.status_code}", + details=metadata, + ) + result = ToolPreInvokeResult( + continue_processing=False, violation=violation, metadata=metadata ) - result = ToolPreInvokeResult(continue_processing=False, violation=violation, metadata=metadata) else: violation = PluginViolation( - reason="Tool Check Unavailable", description="Tool arguments check server returned error:", code=f"checkserver_http_status_code:{response.status_code}", details={} + reason="Tool Check Unavailable", + description="Tool arguments check server returned error:", + code=f"checkserver_http_status_code:{response.status_code}", + details={}, ) result = ToolPreInvokeResult(continue_processing=False, violation=violation) - logger.info(response) - - return ToolPreInvokeResult(continue_processing=True) - async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: + return result + + async def tool_post_invoke( + self, payload: ToolPostInvokePayload, context: PluginContext + ) -> ToolPostInvokeResult: """Plugin hook run after a tool is invoked. Args: From 36f6876196529b574de816a99aa0858265bc4758 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Wed, 11 Feb 2026 15:51:38 +0000 Subject: [PATCH 08/13] logger cleanups Signed-off-by: julianstephen --- plugins/examples/nemocheck/nemocheck/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/examples/nemocheck/nemocheck/plugin.py b/plugins/examples/nemocheck/nemocheck/plugin.py index 82e96a7..c5edce8 100644 --- a/plugins/examples/nemocheck/nemocheck/plugin.py +++ b/plugins/examples/nemocheck/nemocheck/plugin.py @@ -129,7 +129,6 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo reason="Tool Check Unavailable", description="Tool arguments check server returned error:", code=f"checkserver_http_status_code:{response.status_code}", details={} ) result = ToolPreInvokeResult(continue_processing=False, violation=violation) - logger.info(response) return result From 0d6b0d3eb8d522ef45190540283353166a9e308b Mon Sep 17 00:00:00 2001 From: julianstephen Date: Wed, 11 Feb 2026 19:28:29 +0000 Subject: [PATCH 09/13] updated how to use instructions Signed-off-by: julianstephen --- plugins/examples/nemocheckinternal/README.md | 42 +++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/plugins/examples/nemocheckinternal/README.md b/plugins/examples/nemocheckinternal/README.md index 9eab97f..0d64462 100644 --- a/plugins/examples/nemocheckinternal/README.md +++ b/plugins/examples/nemocheckinternal/README.md @@ -1,10 +1,40 @@ -# NemoCheckv2 for Context Forge MCP Gateway - -Nemo Check Adapter. +# Internal NemoCheck Plugin +## Prerequisites: Nemo-check server + * Refer to [orignal repo](https://github.com/m-misiura/demos/tree/main/nemo_openshift/guardrail-checks/deployment) for full instructions + * Instructions adpated for mcpgateway kind cluster to work with an llm proxy routing to some open ai compatable backend below + + ```bash + docker pull quay.io/rh-ee-mmisiura/nemo-guardrails:guardrails_checks_with_tools_o1_v1 + kind load docker-image quay.io/rh-ee-mmisiura/nemo-guardrails:guardrails_checks_with_tools_o1_v1 --name mcp-gateway + cd plugins-adapter/plugins/examples/nemocheck/k8deploy + kubectl apply -f config-tools.yaml + kubectl apply -f server.yaml + ``` ## Installation -1. Copy .env.example .env -2. Enable plugins in `.env` -3. Add the plugin configuration to `plugins/config.yaml`: +1. Find url of nemo-check-server service. E.g., from svc in `server.yaml` +1. Update `${project_root}/resources/config/config.yaml`. Sample file [here](/resources/config/nemocheck-internal-config.yaml) + + ``` +# plugins/config.yaml - Main plugin configuration file +plugins: + - name: "NemoCheckv2" + kind: "plugins.examples.nemocheckinternal.plugin.NemoCheckv2" + description: "Adapter for nemo check server" + version: "0.1.0" + config: + checkserver_url: "http://nemo-guardrails-service:8000/v1/guardrail/checks" +# Plugin directories to scan +plugin_dirs: + - "plugins/examples/nemocheckinternal" # Nemo Check Server plugins + ``` + +1. Ensure plugins->config->checkserver_url points to the correct service +3. Start plugin adapter + +# Test + +1. Open mcp-inspector to the mcp-gateway +1. Try running a tool configured/not configured in nemo check config allow list in configmap [E.g.](/plugins/examples/nemocheck/k8deploy/config-tools.yaml) \ No newline at end of file From 56bc3eed45757ab807bbcb1a1091d66b38c4bb23 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Wed, 11 Feb 2026 22:08:36 +0000 Subject: [PATCH 10/13] nemo check readme updates Signed-off-by: julianstephen --- plugins/examples/README.md | 6 +++++ plugins/examples/nemocheckinternal/README.md | 26 ++++++++++---------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/plugins/examples/README.md b/plugins/examples/README.md index ddf505a..7bc87b0 100644 --- a/plugins/examples/README.md +++ b/plugins/examples/README.md @@ -15,6 +15,12 @@ External plugin adapter for NeMo Guardrails check server - Requires separate NeMo check server deployment - See [nemocheck/README.md](./nemocheck/README.md) for details +### nemocheck-internal +Internal plugin adapter for NeMo Guardrails check server +- **Type**: Internal +- Requires separate NeMo check server deployment +- See [README.md](./nemocheckinternal/README.md) for details + ## Usage Reference plugins in the plugin adapter config (default at `resources/config/config.yaml`): diff --git a/plugins/examples/nemocheckinternal/README.md b/plugins/examples/nemocheckinternal/README.md index 0d64462..89f546b 100644 --- a/plugins/examples/nemocheckinternal/README.md +++ b/plugins/examples/nemocheckinternal/README.md @@ -18,21 +18,21 @@ 1. Update `${project_root}/resources/config/config.yaml`. Sample file [here](/resources/config/nemocheck-internal-config.yaml) ``` -# plugins/config.yaml - Main plugin configuration file -plugins: - - name: "NemoCheckv2" - kind: "plugins.examples.nemocheckinternal.plugin.NemoCheckv2" - description: "Adapter for nemo check server" - version: "0.1.0" - config: - checkserver_url: "http://nemo-guardrails-service:8000/v1/guardrail/checks" -# Plugin directories to scan -plugin_dirs: - - "plugins/examples/nemocheckinternal" # Nemo Check Server plugins + # plugins/config.yaml - Main plugin configuration file + plugins: + - name: "NemoCheckv2" + kind: "plugins.examples.nemocheckinternal.plugin.NemoCheckv2" + description: "Adapter for nemo check server" + version: "0.1.0" + config: + checkserver_url: "http://nemo-guardrails-service:8000/v1/guardrail/checks" + # Plugin directories to scan + plugin_dirs: + - "plugins/examples/nemocheckinternal" # Nemo Check Server plugins ``` -1. Ensure plugins->config->checkserver_url points to the correct service -3. Start plugin adapter +1. In `config.yaml` ensure key `plugins.config.checkserver_url` points to the correct service +1. Start plugin adapter # Test From af32ea62400938e5e7849d24c02a9441a3bcb002 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Thu, 12 Feb 2026 16:07:16 +0000 Subject: [PATCH 11/13] better error message propogation Signed-off-by: julianstephen --- plugins/examples/nemocheckinternal/plugin.py | 13 ++++++------- src/server.py | 6 +++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/plugins/examples/nemocheckinternal/plugin.py b/plugins/examples/nemocheckinternal/plugin.py index b0edb03..51afd44 100644 --- a/plugins/examples/nemocheckinternal/plugin.py +++ b/plugins/examples/nemocheckinternal/plugin.py @@ -26,6 +26,7 @@ import logging import os import requests +import json # Initialize logging service first logger = logging.getLogger(__name__) @@ -55,11 +56,9 @@ def __init__(self, config: PluginConfig): """ global CHECK_ENDPOINT logger.info(f"plugin config {config}") - CHECK_ENDPOINT = config.config.get("checkserver_url", None) - if CHECK_ENDPOINT is None: - CHECK_ENDPOINT = os.getenv( - "CHECK_ENDPOINT", "http://nemo-guardrails-service:8000" - ) + endpoint = config.config.get("checkserver_url", None) + if endpoint is not None: + CHECK_ENDPOINT = endpoint logger.info(f"checkserver at {config}:{CHECK_ENDPOINT}") super().__init__(config) @@ -140,8 +139,8 @@ async def tool_pre_invoke( else: metadata = data.get("rails_status") violation = PluginViolation( - reason=f"Tool Check status:{status}", - description="Rails check blocked request", + reason=f"Check tool rails:{status}.", + description=json.dumps(data), code=f"checkserver_http_status_code:{response.status_code}", details=metadata, ) diff --git a/src/server.py b/src/server.py index 3311b9b..9b462f0 100644 --- a/src/server.py +++ b/src/server.py @@ -69,10 +69,14 @@ async def getToolPreInvokeResponse(body): ) logger.debug(f"**** Tool Pre Invoke Result: {result} ****") if not result.continue_processing: + error_message = "No go - Tool args forbidden" + if result.violation is not None: + violation:PluginViolation = result.violation + error_message = f"{violation.reason} {violation.description}" error_body = { "jsonrpc": body["jsonrpc"], "id": body["id"], - "error": {"code": -32000, "message": "No go - Tool args forbidden"}, + "error": {"code": -32000, "message": error_message}, } body_resp = ep.ProcessingResponse( immediate_response=ep.ImmediateResponse( From 5e50b07bb371a6468aee201fadf737183f2bc973 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Thu, 12 Feb 2026 16:11:40 +0000 Subject: [PATCH 12/13] Minor readme fixes Signed-off-by: julianstephen --- plugins/examples/nemocheckinternal/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/examples/nemocheckinternal/README.md b/plugins/examples/nemocheckinternal/README.md index 89f546b..0e874e2 100644 --- a/plugins/examples/nemocheckinternal/README.md +++ b/plugins/examples/nemocheckinternal/README.md @@ -15,15 +15,17 @@ ## Installation 1. Find url of nemo-check-server service. E.g., from svc in `server.yaml` -1. Update `${project_root}/resources/config/config.yaml`. Sample file [here](/resources/config/nemocheck-internal-config.yaml) +1. Update `${project_root}/resources/config/config.yaml`. Add the blob below, merge if other `plugin`s or `plugin_dir`s already exists. Sample file [here](/resources/config/nemocheck-internal-config.yaml) - ``` + ```yaml # plugins/config.yaml - Main plugin configuration file plugins: - name: "NemoCheckv2" kind: "plugins.examples.nemocheckinternal.plugin.NemoCheckv2" description: "Adapter for nemo check server" - version: "0.1.0" + version: "0.1.0" + hooks: ["tool_pre_invoke", "tool_post_invoke"] + mode: "enforce" # enforce | permissive | disabled config: checkserver_url: "http://nemo-guardrails-service:8000/v1/guardrail/checks" # Plugin directories to scan From 7ece9e92887b772d22f98ec3d5ace498060d7259 Mon Sep 17 00:00:00 2001 From: julianstephen Date: Thu, 12 Feb 2026 16:19:30 +0000 Subject: [PATCH 13/13] linting fixes Signed-off-by: julianstephen --- plugins/examples/nemocheckinternal/plugin.py | 2 +- src/server.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/examples/nemocheckinternal/plugin.py b/plugins/examples/nemocheckinternal/plugin.py index 51afd44..30b3223 100644 --- a/plugins/examples/nemocheckinternal/plugin.py +++ b/plugins/examples/nemocheckinternal/plugin.py @@ -58,7 +58,7 @@ def __init__(self, config: PluginConfig): logger.info(f"plugin config {config}") endpoint = config.config.get("checkserver_url", None) if endpoint is not None: - CHECK_ENDPOINT = endpoint + CHECK_ENDPOINT = endpoint logger.info(f"checkserver at {config}:{CHECK_ENDPOINT}") super().__init__(config) diff --git a/src/server.py b/src/server.py index 9b462f0..fca8319 100644 --- a/src/server.py +++ b/src/server.py @@ -18,7 +18,9 @@ PromptPrehookPayload, ToolPostInvokePayload, ToolPreInvokePayload, + PluginViolation, ) + from mcpgateway.plugins.framework import PluginManager from mcpgateway.plugins.framework.models import GlobalContext @@ -71,8 +73,8 @@ async def getToolPreInvokeResponse(body): if not result.continue_processing: error_message = "No go - Tool args forbidden" if result.violation is not None: - violation:PluginViolation = result.violation - error_message = f"{violation.reason} {violation.description}" + violation: PluginViolation = result.violation + error_message = f"{violation.reason} -- {violation.description}" error_body = { "jsonrpc": body["jsonrpc"], "id": body["id"],