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
9 changes: 5 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ RUN if [ -n "$PLUGIN_DEPS" ]; then \
echo "$PLUGIN_DEPS" | tr ',' '\n' | while read plugin; do \
plugin=$(echo "$plugin" | xargs); \
if [ -n "$plugin" ]; then \
req_file="plugins/examples/$plugin/requirements.txt"; \
plugin_dir="plugins/examples/$plugin"; \
req_file="$plugin_dir/pyproject.toml"; \
if [ -f "$req_file" ]; then \
echo "Installing dependencies from $req_file"; \
pip install --no-cache-dir -r "$req_file"; \
echo "Installing dependencies from $plugin_dir"; \
pip install --no-cache-dir $plugin_dir; \
else \
echo "Warning: No requirements.txt found for plugin '$plugin' at $req_file"; \
echo "Warning: No pyproject.toml found for plugin '$plugin' at $req_file"; \
fi; \
fi; \
done; \
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ endif

# Build the combined broker and router
build:
$(CONTAINER_RUNTIME) build -t $(IMAGE_LOCAL) .
$(CONTAINER_RUNTIME) build -t $(IMAGE_LOCAL) . --build-arg PLUGIN_DEPS=${PLUGIN_DEPS}

load:
kind load docker-image $(IMAGE_LOCAL) --name mcp-gateway
Expand Down Expand Up @@ -72,3 +72,7 @@ deploy_quay:
kubectl apply -f ext-proc.yaml
kubectl apply -f filter.yaml

dev-run-nemocheck:
export PYTHONPATH="${PYTHONPATH}:."; \
pip install -e plugins/examples/nemocheck; \
python src/server.py
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ An Envoy external processor (ext-proc) for configuring and invoking guardrails i

3. **Deploy to kind cluster**
```bash
make all
make all PLUGIN_DEPS=nemocheck #replace nemocheck with comma seperated list of plugins to include other plugins
```

See [detailed build instructions](./docs/build.md) for manual build steps.
Expand Down
3 changes: 2 additions & 1 deletion plugins/examples/nemocheck/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"""

# First-Party
from mcpgateway.plugins.framework import (
from cpex.framework import (
Plugin,
PluginConfig,
PluginContext,
Expand Down Expand Up @@ -122,6 +122,7 @@ async def tool_pre_invoke(
)

tool_name = payload.name
assert payload.args is not None
check_nemo_payload = {
"model": self.model_name,
"messages": [
Expand Down
9 changes: 5 additions & 4 deletions plugins/examples/nemocheck/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ authors = [
]

dependencies = [
"cpex",
"mcp>=1.16.0",
"mcp-contextforge-gateway",
"requests>=2.32.5",
]

# URLs
Expand All @@ -56,9 +57,6 @@ Repository = "https://github.com/IBM/mcp-context-forge"
"Bug Tracker" = "https://github.com/IBM/mcp-context-forge/issues"
Changelog = "https://github.com/IBM/mcp-context-forge/blob/main/CHANGELOG.md"

[tool.uv.sources]
mcp-contextforge-gateway = { git = "https://github.com/IBM/mcp-context-forge.git", rev = "main" }

# ----------------------------------------------------------------
# Optional dependency groups (extras)
# ----------------------------------------------------------------
Expand Down Expand Up @@ -97,6 +95,9 @@ nemocheck = [
"resources/plugins/config.yaml",
]

[tool.uv.sources]
cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git" }

[dependency-groups]
dev = [
"pytest>=8.4.2",
Expand Down
89 changes: 86 additions & 3 deletions plugins/examples/nemocheck/tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@

# Third-Party
import pytest
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, Union
from pydantic import BaseModel, Field

# First-Party
from mcpgateway.common.models import Message, PromptResult, Role, TextContent
from mcpgateway.plugins.framework import (
# from mcpgateway.common.models import Message, PromptResult, Role, TextContent
from cpex.framework import (
GlobalContext,
PluginManager,
PromptHookType,
Expand All @@ -20,6 +23,85 @@
)


##----- Temporary classes from contextforge-plugins-framework/tests/unit/cpex/fixtures/common/models.py ##
## Available at https://github.com/contextforge-org/contextforge-plugins-framework/blob/5769b1bbced23cdc7448bf001aecdbe6a44f22d5/tests/unit/cpex/fixtures/common/models.py
class Role(str, Enum):
"""Message role in conversations."""

ASSISTANT = "assistant"
USER = "user"


# Base content types
class TextContent(BaseModel):
"""Text content for messages (MCP spec-compliant)."""

type: Literal["text"]
text: str
annotations: Optional[Any] = None
meta: Optional[Dict[str, Any]] = Field(None, alias="_meta")


class ResourceContents(BaseModel):
"""Base class for resource contents (MCP spec-compliant)."""

uri: str
mime_type: Optional[str] = Field(None, alias="mimeType")
meta: Optional[Dict[str, Any]] = Field(None, alias="_meta")


# Legacy ResourceContent for backwards compatibility
class ResourceContent(BaseModel):
"""Resource content that can be embedded (LEGACY - use TextResourceContents or BlobResourceContents)."""

type: Literal["resource"]
id: str
uri: str
mime_type: Optional[str] = None
text: Optional[str] = None
blob: Optional[bytes] = None


ContentType = Union[TextContent, ResourceContent]


# Message types
class Message(BaseModel):
"""A message in a conversation.

Attributes:
role (Role): The role of the message sender.
content (ContentType): The content of the message.
"""

role: Role
content: ContentType


class PromptMessage(BaseModel):
"""Message in a prompt (MCP spec-compliant)."""

role: Role
content: "ContentBlock" # Uses ContentBlock union (includes ResourceLink and EmbeddedResource)


class PromptResult(BaseModel):
"""Result of rendering a prompt template.

Attributes:
messages (List[Message]): The list of messages produced by rendering the prompt.
description (Optional[str]): An optional description of the rendered result.
"""

messages: List[Message]
description: Optional[str] = None


# MCP spec-compliant ContentBlock union for prompts and tool results
# Per spec: ContentBlock can include ResourceLink and EmbeddedResource
ContentBlock = Union[TextContent]


@pytest.fixture(scope="module", autouse=True)
def plugin_manager():
"""Initialize plugin manager."""
Expand Down Expand Up @@ -49,7 +131,8 @@ async def test_prompt_post_hook(plugin_manager: PluginManager):
"""Test prompt post hook across all registered plugins."""
# Customize payload for testing
message = Message(
content=TextContent(type="text", text="prompt"), role=Role.USER
content=TextContent(type="text", text="prompt", _meta={}),
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.

q - is this necessary? can it just default to empty?

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.

I remember seeing syntax error without it.

role=Role.USER,
)
prompt_result = PromptResult(messages=[message])
payload = PromptPosthookPayload(
Expand Down
2 changes: 1 addition & 1 deletion plugins/examples/nemocheck/tests/test_nemocheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest

# First-Party
from mcpgateway.plugins.framework import (
from cpex.framework import (
PluginConfig,
PluginContext,
GlobalContext,
Expand Down
Loading