Skip to content
Open
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
19 changes: 19 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,25 @@ agentic_security --port=PORT --host=HOST

<img width="100%" alt="booking-screen" src="https://raw.githubusercontent.com/msoedov/agentic_security/refs/heads/main/docs/images/demo.gif">

## MCP client example

Agentic Security includes an MCP stdio server in `agentic_security.mcp.main`.
To list the available MCP tools from a local checkout:

```shell
python examples/mcp_client_usage.py
```

To call HTTP-backed tools, run the Agentic Security app first, then point the
MCP server at it:

```shell
agentic_security --host 127.0.0.1 --port 8718
python examples/mcp_client_usage.py --agentic-security-url http://127.0.0.1:8718 --call get_spec_templates
```

See `docs/mcp_client_usage.md` for the full walkthrough.

## LLM kwargs

Agentic Security uses plain text HTTP spec like:
Expand Down
40 changes: 13 additions & 27 deletions agentic_security/mcp/client.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import asyncio
import sys

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

from agentic_security.logutils import logger

# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="python", # Executable
args=["agentic_security/mcp/main.py"], # Your server script
env=None, # Optional environment variables
)

def build_server_params() -> StdioServerParameters:
"""Create server parameters for a stdio MCP client session."""
return StdioServerParameters(
command=sys.executable,
args=["-m", "agentic_security.mcp.main"],
env=None,
)


async def run() -> None:
try:
server_params = build_server_params()
logger.info(
"Starting stdio client session with server parameters: %s", server_params
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize the connection --> connection does not work
logger.info("Initializing client session...")
await session.initialize()

# List available prompts, resources, and tools --> no avalialbe tools
logger.info("Listing available prompts...")
prompts = await session.list_prompts()
logger.info(f"Available prompts: {prompts}")
Expand All @@ -36,26 +38,10 @@ async def run() -> None:
logger.info("Listing available tools...")
tools = await session.list_tools()
logger.info(f"Available tools: {tools}")

# Call the echo tool --> echo tool issue
logger.info("Calling echo_tool with message...")
echo_result = await session.call_tool(
"echo_tool", arguments={"message": "Hello from client!"}
logger.info(
"Available MCP tool names: %s",
", ".join(tool.name for tool in tools.tools),
)
logger.info(f"Tool result: {echo_result}")

# # Read the echo resource
# echo_content, mime_type = await session.read_resource(
# "echo://Hello_resource"
# )
# logger.info(f"Resource content: {echo_content}")
# logger.info(f"Resource MIME type: {mime_type}")

# # Get and use the echo prompt
# prompt_result = await session.get_prompt(
# "echo_prompt", arguments={"message": "Hello prompt!"}
# )
# logger.info(f"Prompt result: {prompt_result}")

logger.info("Client operations completed successfully.")
return prompts, resources, tools
Expand Down
4 changes: 3 additions & 1 deletion agentic_security/mcp/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

import httpx
from mcp.server.fastmcp import FastMCP

Expand All @@ -8,7 +10,7 @@
)

# FastAPI Server Configuration
AGENTIC_SECURITY = "http://0.0.0.0:8718"
AGENTIC_SECURITY = os.getenv("AGENTIC_SECURITY_URL", "http://0.0.0.0:8718")


@mcp.tool()
Expand Down
65 changes: 65 additions & 0 deletions docs/mcp_client_usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# MCP client usage

Agentic Security exposes an MCP stdio server in `agentic_security.mcp.main`.
The example client in `examples/mcp_client_usage.py` shows how to connect to
that server, list available tools, and optionally call simple no-argument tools.

## List MCP tools

From the repository root:

```shell
python examples/mcp_client_usage.py
```

This starts the MCP server as a subprocess with:

```shell
python -m agentic_security.mcp.main
```

The client initializes an MCP session and prints the available Agentic Security
tools, including `verify_llm`, `start_scan`, `stop_scan`, `get_data_config`, and
`get_spec_templates`.

## Call an HTTP-backed tool

Some MCP tools call the Agentic Security HTTP app. Start the app in another
terminal first:

```shell
agentic_security --host 127.0.0.1 --port 8718
```

Then point the MCP server at that app and call a no-argument tool:

```shell
python examples/mcp_client_usage.py \
--agentic-security-url http://127.0.0.1:8718 \
--call get_spec_templates
```

You can also set `AGENTIC_SECURITY_URL` directly:

```shell
AGENTIC_SECURITY_URL=http://127.0.0.1:8718 python examples/mcp_client_usage.py --call get_data_config
```

## Use the package helper

For tests or quick local checks, `agentic_security.mcp.client.run()` creates the
same stdio session and returns the prompt, resource, and tool list results:

```python
import asyncio

from agentic_security.mcp.client import run


async def main() -> None:
_prompts, _resources, tools = await run()
print([tool.name for tool in tools.tools])


asyncio.run(main())
```
103 changes: 103 additions & 0 deletions examples/mcp_client_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Example MCP client for the Agentic Security stdio server.

The default command lists the tools exposed by ``agentic_security.mcp.main``.
If the Agentic Security HTTP app is running, pass ``--call`` to invoke one of
the no-argument HTTP-backed tools through MCP.
"""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import sys
from typing import Any

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


NO_ARGUMENT_TOOLS = {"get_data_config", "get_spec_templates", "stop_scan"}


def _build_server_params(agentic_security_url: str | None) -> StdioServerParameters:
env = os.environ.copy()
if agentic_security_url:
env["AGENTIC_SECURITY_URL"] = agentic_security_url

return StdioServerParameters(
command=sys.executable,
args=["-m", "agentic_security.mcp.main"],
env=env,
)


def _jsonable(value: Any) -> Any:
if hasattr(value, "model_dump"):
return value.model_dump(mode="json")
if isinstance(value, (list, tuple)):
return [_jsonable(item) for item in value]
if isinstance(value, dict):
return {key: _jsonable(item) for key, item in value.items()}
return value


async def run_client(agentic_security_url: str | None, call_tool: str | None) -> None:
server_params = _build_server_params(agentic_security_url)

async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
tool_names = [tool.name for tool in tools.tools]

print("Available Agentic Security MCP tools:")
for tool in tools.tools:
description_lines = (tool.description or "").strip().splitlines()
description = description_lines[0] if description_lines else "No description"
print(f"- {tool.name}: {description}")

if not call_tool:
return

if call_tool not in tool_names:
raise ValueError(
f"Unknown tool {call_tool!r}. Available tools: {', '.join(tool_names)}"
)
if call_tool not in NO_ARGUMENT_TOOLS:
raise ValueError(
f"{call_tool!r} requires arguments. This example only calls "
f"no-argument tools: {', '.join(sorted(NO_ARGUMENT_TOOLS))}"
)

result = await session.call_tool(call_tool, arguments={})
print()
print(f"{call_tool} result:")
print(json.dumps(_jsonable(result), indent=2))


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="List Agentic Security MCP tools and optionally call one.",
)
parser.add_argument(
"--agentic-security-url",
default=None,
help=(
"Agentic Security HTTP app URL. Defaults to AGENTIC_SECURITY_URL "
"or http://0.0.0.0:8718 in the server."
),
)
parser.add_argument(
"--call",
choices=sorted(NO_ARGUMENT_TOOLS),
help="Optional no-argument MCP tool to call after listing tools.",
)
return parser.parse_args()


if __name__ == "__main__":
args = parse_args()
asyncio.run(run_client(args.agentic_security_url, args.call))
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ nav:
- Dataset Extension: datasets.md
- External Modules: external_module.md
- CI/CD Integration: ci_cd.md
- MCP Client Usage: mcp_client_usage.md
- Bayesian Optimization: optimizer.md
- Image Generation: image_generation.md
- Stenography Functions: stenography.md
Expand Down
18 changes: 13 additions & 5 deletions tests/unit/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@


@pytest.mark.asyncio
async def test_mcp_echo_tool():
"""Test the echo tool functionality"""
async def test_mcp_client_lists_agentic_security_tools():
"""Test that the MCP client can discover the server tools."""
prompts, resources, tools = await run()
assert prompts
assert resources
assert tools
tool_names = {tool.name for tool in tools.tools}

assert prompts is not None
assert resources is not None
assert {
"verify_llm",
"start_scan",
"stop_scan",
"get_data_config",
"get_spec_templates",
}.issubset(tool_names)