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/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 diff --git a/plugins/examples/nemocheckinternal/README.md b/plugins/examples/nemocheckinternal/README.md new file mode 100644 index 0000000..0e874e2 --- /dev/null +++ b/plugins/examples/nemocheckinternal/README.md @@ -0,0 +1,42 @@ +# 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. Find url of nemo-check-server service. E.g., from svc in `server.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" + 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 + plugin_dirs: + - "plugins/examples/nemocheckinternal" # Nemo Check Server plugins + ``` + +1. In `config.yaml` ensure key `plugins.config.checkserver_url` points to the correct service +1. 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 diff --git a/plugins/examples/nemocheckinternal/__init__.py b/plugins/examples/nemocheckinternal/__init__.py new file mode 100644 index 0000000..16e5be5 --- /dev/null +++ b/plugins/examples/nemocheckinternal/__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/nemocheckinternal/config.yaml b/plugins/examples/nemocheckinternal/config.yaml new file mode 100644 index 0000000..d930000 --- /dev/null +++ b/plugins/examples/nemocheckinternal/config.yaml @@ -0,0 +1,28 @@ +plugins: + - name: "NemoCheckv2" + kind: "plugins.examples.nemocheckinternal.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/nemocheckinternal/plugin-manifest.yaml b/plugins/examples/nemocheckinternal/plugin-manifest.yaml new file mode 100644 index 0000000..d8fa01f --- /dev/null +++ b/plugins/examples/nemocheckinternal/plugin-manifest.yaml @@ -0,0 +1,10 @@ +description: "Nemo Check Adapter" +name: NemoCheckv2 +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/nemocheckinternal/plugin.py b/plugins/examples/nemocheckinternal/plugin.py new file mode 100644 index 0000000..30b3223 --- /dev/null +++ b/plugins/examples/nemocheckinternal/plugin.py @@ -0,0 +1,174 @@ +"""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, + PluginViolation, +) + +import logging +import os +import requests +import json + +# 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") + + +headers = { + "Content-Type": "application/json", +} + + +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 + """ + global CHECK_ENDPOINT + logger.info(f"plugin config {config}") + 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) + + 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"Check tool rails:{status}.", + description=json.dumps(data), + 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) + + return result + + 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) diff --git a/resources/config/config.yaml b/resources/config/config.yaml index ef3be7d..4b1530f 100644 --- a/resources/config/config.yaml +++ b/resources/config/config.yaml @@ -22,6 +22,7 @@ plugins: replace: crud - search: crud replace: yikes + # Nemo example - name: "NemoWrapperPlugin" kind: "plugins.examples.nemo.nemo_wrapper_plugin.NemoWrapperPlugin" @@ -35,12 +36,21 @@ plugins: config: foo: bar + # Nemo Check Example + - name: "NemoCheckv2" + kind: "plugins.examples.nemocheckinternal.plugin.NemoCheckv2" + description: "Adapter for nemo check server" + version: "0.1.0" + author: "Julian Stephen" + 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: @@ -48,4 +58,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 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 diff --git a/src/server.py b/src/server.py index 3311b9b..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 @@ -69,10 +71,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(