Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions plugins/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down
1 change: 0 additions & 1 deletion plugins/examples/nemocheck/nemocheck/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
42 changes: 42 additions & 0 deletions plugins/examples/nemocheckinternal/README.md
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions plugins/examples/nemocheckinternal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""MCP Gateway NemoCheckv2 Plugin - Nemo Check Adapter.

Copyright 2025
SPDX-License-Identifier: Apache-2.0
Authors: julianstephen

"""
28 changes: 28 additions & 0 deletions plugins/examples/nemocheckinternal/config.yaml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions plugins/examples/nemocheckinternal/plugin-manifest.yaml
Original file line number Diff line number Diff line change
@@ -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:
174 changes: 174 additions & 0 deletions plugins/examples/nemocheckinternal/plugin.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: how distinct is the plugin implementation here vs. the external plugin itself https://github.com/kagenti/plugins-adapter/blob/main/plugins/examples/nemocheck/nemocheck/plugin.py ?

I was expecting that we would be able to use similar code but only the packaging (referencing the class path here vs. packaging up as a server for external) would be different?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, but each of the example is a bit independent and cross dependency may not be nice. We can refactor out the call-nemo-check logic outside both, but where do we house it? Will have to make some common-plugin-utils that needs to be an independent module that can be used in in both plugins.

Another option is to simply keep the nemo internal plugin and add some packaging logic for the external one to pull the code from the internal plugin. The external one has a full project structure that can be containerized etc. Or we can simply nix the external one and fix some of the v2 naming in the internal one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is to simply keep the nemo internal plugin and add some packaging logic for the external one to pull the code from the internal plugin.

Yes I was thinking along these lines, especially as an example this seems easier to present as "internal first" rather than generally expecting the two separate servers deployed with the external plugin, but we can keep the extra packaging logic as an example, without having the code duplicated. The v2 naming is confusing if there's not fundamental plugin or API changes

side note we can probably move the nemoguardrails requirement out from default requirements afterward https://github.com/kagenti/plugins-adapter/blob/main/requirements.txt

"""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)
12 changes: 11 additions & 1 deletion resources/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ plugins:
replace: crud
- search: crud
replace: yikes

# Nemo example
- name: "NemoWrapperPlugin"
kind: "plugins.examples.nemo.nemo_wrapper_plugin.NemoWrapperPlugin"
Expand All @@ -35,17 +36,26 @@ 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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the relevant hooks be present here?

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
plugin_health_check_interval: 60
30 changes: 30 additions & 0 deletions resources/config/nemocheck-internal-config.yaml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 7 additions & 1 deletion src/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
PromptPrehookPayload,
ToolPostInvokePayload,
ToolPreInvokePayload,
PluginViolation,
)

from mcpgateway.plugins.framework import PluginManager
from mcpgateway.plugins.framework.models import GlobalContext

Expand Down Expand Up @@ -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(
Expand Down