From 72f0f63a891148999d5d3e0d0643c4bd3b96e8ce Mon Sep 17 00:00:00 2001 From: JackSpiece Date: Tue, 19 May 2026 19:16:11 +0800 Subject: [PATCH] docs: add MCP client usage examples --- Readme.md | 19 ++++++ agentic_security/mcp/client.py | 40 +++++-------- agentic_security/mcp/main.py | 4 +- docs/mcp_client_usage.md | 65 +++++++++++++++++++++ examples/mcp_client_usage.py | 103 +++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + tests/unit/test_mcp.py | 18 ++++-- 7 files changed, 217 insertions(+), 33 deletions(-) create mode 100644 docs/mcp_client_usage.md create mode 100644 examples/mcp_client_usage.py diff --git a/Readme.md b/Readme.md index 97efb9c..a731c9a 100644 --- a/Readme.md +++ b/Readme.md @@ -83,6 +83,25 @@ agentic_security --port=PORT --host=HOST booking-screen +## 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: diff --git a/agentic_security/mcp/client.py b/agentic_security/mcp/client.py index e8a29cc..b3d9a05 100644 --- a/agentic_security/mcp/client.py +++ b/agentic_security/mcp/client.py @@ -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}") @@ -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 diff --git a/agentic_security/mcp/main.py b/agentic_security/mcp/main.py index 7e19e2c..858da5f 100644 --- a/agentic_security/mcp/main.py +++ b/agentic_security/mcp/main.py @@ -1,3 +1,5 @@ +import os + import httpx from mcp.server.fastmcp import FastMCP @@ -8,7 +10,7 @@ mcp = FastMCP( ) # 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() diff --git a/docs/mcp_client_usage.md b/docs/mcp_client_usage.md new file mode 100644 index 0000000..55109d7 --- /dev/null +++ b/docs/mcp_client_usage.md @@ -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()) +``` diff --git a/examples/mcp_client_usage.py b/examples/mcp_client_usage.py new file mode 100644 index 0000000..e13b445 --- /dev/null +++ b/examples/mcp_client_usage.py @@ -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)) diff --git a/mkdocs.yml b/mkdocs.yml index 9e7ac41..2317754 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/tests/unit/test_mcp.py b/tests/unit/test_mcp.py index 23f8f53..2d0adbb 100644 --- a/tests/unit/test_mcp.py +++ b/tests/unit/test_mcp.py @@ -4,9 +4,17 @@ from agentic_security.mcp.client import run @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)