|  | 
|  | 1 | +{ | 
|  | 2 | + "cells": [ | 
|  | 3 | +  { | 
|  | 4 | +   "cell_type": "markdown", | 
|  | 5 | +   "id": "f7f79ace", | 
|  | 6 | +   "metadata": {}, | 
|  | 7 | +   "source": [ | 
|  | 8 | +    "# POC – MCP Tool Registration\n", | 
|  | 9 | +    "\n", | 
|  | 10 | +    "This example aspires to verify the points listed in [POC - AI with Splunk Apps](https://cisco-my.sharepoint.com/:w:/r/personal/hbalacha_cisco_com/Documents/POC%20-%20AI%20with%20Splunk%20Apps.docx?d=w2776e089011943abbd84c0fa30a53f34&csf=1&web=1&e=RxvShR)\n", | 
|  | 11 | +    "\n", | 
|  | 12 | +    "- Develop @tool Decorator\n", | 
|  | 13 | +    "  - [ ] Capture e.g. tool_name, description, inputs, outputs\n", | 
|  | 14 | +    "- MCP JSONSchema can (and most probably should) be used for tool registration in Splunk\n" | 
|  | 15 | +   ] | 
|  | 16 | +  }, | 
|  | 17 | +  { | 
|  | 18 | +   "cell_type": "code", | 
|  | 19 | +   "execution_count": null, | 
|  | 20 | +   "id": "c349a502", | 
|  | 21 | +   "metadata": {}, | 
|  | 22 | +   "outputs": [], | 
|  | 23 | +   "source": [ | 
|  | 24 | +    "from fastmcp import Client\n", | 
|  | 25 | +    "from mcp.types import Tool\n", | 
|  | 26 | +    "\n", | 
|  | 27 | +    "MCP_SERVER_HOST: str = \"0.0.0.0\"\n", | 
|  | 28 | +    "MCP_SERVER_PORT: int = 2137\n", | 
|  | 29 | +    "\n", | 
|  | 30 | +    "\n", | 
|  | 31 | +    "conn_str = f\"http://{MCP_SERVER_HOST}:{MCP_SERVER_PORT}/mcp\"\n", | 
|  | 32 | +    "mcp_client = Client(\"./tools.py\")\n", | 
|  | 33 | +    "\n", | 
|  | 34 | +    "\n", | 
|  | 35 | +    "async def get_tools() -> list[Tool]:\n", | 
|  | 36 | +    "    tools = []\n", | 
|  | 37 | +    "    async with mcp_client:\n", | 
|  | 38 | +    "        tools = await mcp_client.list_tools()\n", | 
|  | 39 | +    "\n", | 
|  | 40 | +    "    return tools\n", | 
|  | 41 | +    "\n", | 
|  | 42 | +    "\n", | 
|  | 43 | +    "call_tool_result = await get_tools()\n", | 
|  | 44 | +    "for tool in call_tool_result:\n", | 
|  | 45 | +    "    print(tool.name)\n", | 
|  | 46 | +    "    print(tool.description)\n", | 
|  | 47 | +    "    print(tool.inputSchema)\n", | 
|  | 48 | +    "    print(tool.outputSchema)\n", | 
|  | 49 | +    "    print(tool.meta)" | 
|  | 50 | +   ] | 
|  | 51 | +  }, | 
|  | 52 | +  { | 
|  | 53 | +   "cell_type": "markdown", | 
|  | 54 | +   "id": "cd1da846", | 
|  | 55 | +   "metadata": {}, | 
|  | 56 | +   "source": [ | 
|  | 57 | +    "- execution_mode (external_http)\n", | 
|  | 58 | +    "  - Is this what MCP calls `transports`?\n", | 
|  | 59 | +    "- execution_metadata (endpoint URL)\n", | 
|  | 60 | +    "  - Isn't that just `/execute_tool` or `tool/call`?" | 
|  | 61 | +   ] | 
|  | 62 | +  }, | 
|  | 63 | +  { | 
|  | 64 | +   "cell_type": "code", | 
|  | 65 | +   "execution_count": null, | 
|  | 66 | +   "id": "931ff98f", | 
|  | 67 | +   "metadata": {}, | 
|  | 68 | +   "outputs": [], | 
|  | 69 | +   "source": [ | 
|  | 70 | +    "mcp_client = Client(\"tools.py\")\n", | 
|  | 71 | +    "\n", | 
|  | 72 | +    "\n", | 
|  | 73 | +    "async def call_tool(tool_name: str, arguments: dict[str, int]):\n", | 
|  | 74 | +    "    result = None\n", | 
|  | 75 | +    "    async with mcp_client:\n", | 
|  | 76 | +    "        result = await mcp_client.call_tool(tool_name, arguments)\n", | 
|  | 77 | +    "\n", | 
|  | 78 | +    "    return result\n", | 
|  | 79 | +    "\n", | 
|  | 80 | +    "\n", | 
|  | 81 | +    "call_tool_result = await call_tool(\"generating_csc\", {\"count\": 10})\n", | 
|  | 82 | +    "\n", | 
|  | 83 | +    "[print(data) for data in call_tool_result.data]\n" | 
|  | 84 | +   ] | 
|  | 85 | +  }, | 
|  | 86 | +  { | 
|  | 87 | +   "cell_type": "markdown", | 
|  | 88 | +   "id": "2da04c9f", | 
|  | 89 | +   "metadata": {}, | 
|  | 90 | +   "source": [ | 
|  | 91 | +    "- [ ] Support YAML v2 Tool Definition\n", | 
|  | 92 | +    "- [ ] Merge decorator + YAML for complete metadata\n", | 
|  | 93 | +    "\n", | 
|  | 94 | +    "- Implement Post-Install Script (sdk_post_install.py):\n", | 
|  | 95 | +    "  - [ ] Load app modules → decorators populate RegisteredTools\n", | 
|  | 96 | +    "  - [ ] Merge YAML/decorator metadata\n", | 
|  | 97 | +    "  - [ ] Build MCP /tools/register payload\n", | 
|  | 98 | +    "  - [ ] Call MCP registry with credentials (from App Manager)\n", | 
|  | 99 | +    "  - [ ] Log success/fail to MCP audit\n" | 
|  | 100 | +   ] | 
|  | 101 | +  }, | 
|  | 102 | +  { | 
|  | 103 | +   "cell_type": "code", | 
|  | 104 | +   "execution_count": null, | 
|  | 105 | +   "id": "2e594272", | 
|  | 106 | +   "metadata": {}, | 
|  | 107 | +   "outputs": [ | 
|  | 108 | +    { | 
|  | 109 | +     "name": "stdout", | 
|  | 110 | +     "output_type": "stream", | 
|  | 111 | +     "text": [ | 
|  | 112 | +      "/Users/bjedreck/Projects/spl-mcp-tool/bin/../default/app.conf\n", | 
|  | 113 | +      "name='aws_logs_search' title=None description='Execute SPL queries against AWS logs' inputSchema={'type': 'object', 'properties': {}, 'required': []} outputSchema={'type': 'object', 'properties': {}, 'required': []} icons=None annotations=None meta={'permissions': ['role:search_admin', 'role:aws_analyst'], 'tool_type': 'search', 'schema_version': '1.0'}\n" | 
|  | 114 | +     ] | 
|  | 115 | +    } | 
|  | 116 | +   ], | 
|  | 117 | +   "source": [ | 
|  | 118 | +    "import configparser\n", | 
|  | 119 | +    "import os\n", | 
|  | 120 | +    "from dataclasses import asdict, dataclass, field\n", | 
|  | 121 | +    "from typing import Any, Literal\n", | 
|  | 122 | +    "\n", | 
|  | 123 | +    "from mcp.types import Tool\n", | 
|  | 124 | +    "\n", | 
|  | 125 | +    "\n", | 
|  | 126 | +    "@dataclass\n", | 
|  | 127 | +    "class SplunkMeta:\n", | 
|  | 128 | +    "    permissions: list[str]\n", | 
|  | 129 | +    "    tool_type: str\n", | 
|  | 130 | +    "    schema_version: str\n", | 
|  | 131 | +    "\n", | 
|  | 132 | +    "\n", | 
|  | 133 | +    "@dataclass\n", | 
|  | 134 | +    "class McpInputOutputSchema:\n", | 
|  | 135 | +    "    type: Literal[\"object\"] = \"object\"\n", | 
|  | 136 | +    "    properties: dict[str, Any] = field(default_factory=lambda: {})  # pyright: ignore[reportExplicitAny]\n", | 
|  | 137 | +    "    required: list[str] = field(default_factory=lambda: [])\n", | 
|  | 138 | +    "\n", | 
|  | 139 | +    "\n", | 
|  | 140 | +    "tool_reg_prefix = \"app:mcp_tool\"\n", | 
|  | 141 | +    "\n", | 
|  | 142 | +    "\n", | 
|  | 143 | +    "def filter_sections(section_name: str):\n", | 
|  | 144 | +    "    return section_name.startswith(tool_reg_prefix)\n", | 
|  | 145 | +    "\n", | 
|  | 146 | +    "\n", | 
|  | 147 | +    "def match_input_schema(input: Literal[\"query_string\"] | Literal[\"other\"]):\n", | 
|  | 148 | +    "    match input:\n", | 
|  | 149 | +    "        case \"query_string\":\n", | 
|  | 150 | +    "            return {\n", | 
|  | 151 | +    "                \"type\": \"object\",\n", | 
|  | 152 | +    "                \"properties\": {\n", | 
|  | 153 | +    "                    \"query_string\": {\n", | 
|  | 154 | +    "                        \"type\": \"string\",\n", | 
|  | 155 | +    "                        \"description\": \"SPL2 query string\",\n", | 
|  | 156 | +    "                    }\n", | 
|  | 157 | +    "                },\n", | 
|  | 158 | +    "            }\n", | 
|  | 159 | +    "        case _:\n", | 
|  | 160 | +    "            raise NotImplementedError(\"We don't know what to put here lol\")\n", | 
|  | 161 | +    "\n", | 
|  | 162 | +    "\n", | 
|  | 163 | +    "def parse_app_conf_tool_registrations(file_path: str) -> list[Tool]:\n", | 
|  | 164 | +    "    config = configparser.ConfigParser()\n", | 
|  | 165 | +    "    all_sections_len = config.read(file_path)\n", | 
|  | 166 | +    "    if len(all_sections_len) == 0:\n", | 
|  | 167 | +    "        return []\n", | 
|  | 168 | +    "\n", | 
|  | 169 | +    "    tool_reg_sections: list[str] = list(filter(filter_sections, config.sections()))\n", | 
|  | 170 | +    "    if len(tool_reg_sections) == 0:\n", | 
|  | 171 | +    "        return []\n", | 
|  | 172 | +    "\n", | 
|  | 173 | +    "    ini_tools: list[Tool] = []\n", | 
|  | 174 | +    "    for reg_section in tool_reg_sections:\n", | 
|  | 175 | +    "        reg_section_data = config[reg_section]\n", | 
|  | 176 | +    "\n", | 
|  | 177 | +    "        name: str = reg_section.split(\":\")[2]\n", | 
|  | 178 | +    "        description = reg_section_data[\"description\"]\n", | 
|  | 179 | +    "        # https://modelcontextprotocol.io/specification/2025-06-18/schema#tool\n", | 
|  | 180 | +    "        inputSchema = McpInputOutputSchema(properties={}, required=[])\n", | 
|  | 181 | +    "        outputSchema = McpInputOutputSchema(properties={}, required=[])\n", | 
|  | 182 | +    "        meta = SplunkMeta(\n", | 
|  | 183 | +    "            permissions=[\n", | 
|  | 184 | +    "                perm.strip()\n", | 
|  | 185 | +    "                for perm in reg_section_data[\"permissions\"].strip().split(\",\")\n", | 
|  | 186 | +    "            ],\n", | 
|  | 187 | +    "            tool_type=\"search\",\n", | 
|  | 188 | +    "            schema_version=reg_section_data[\"schema_version\"].strip(),\n", | 
|  | 189 | +    "        )\n", | 
|  | 190 | +    "\n", | 
|  | 191 | +    "        ini_tool = Tool(\n", | 
|  | 192 | +    "            name=name,\n", | 
|  | 193 | +    "            description=description,\n", | 
|  | 194 | +    "            inputSchema=asdict(inputSchema),\n", | 
|  | 195 | +    "            outputSchema=asdict(outputSchema),\n", | 
|  | 196 | +    "            _meta=asdict(meta),\n", | 
|  | 197 | +    "        )\n", | 
|  | 198 | +    "        ini_tools.append(ini_tool)\n", | 
|  | 199 | +    "\n", | 
|  | 200 | +    "    return ini_tools\n", | 
|  | 201 | +    "\n", | 
|  | 202 | +    "\n", | 
|  | 203 | +    "async def post_install():\n", | 
|  | 204 | +    "    curr_path = os.path.join(os.getcwd(), \"..\", \"default\", \"app.conf\")\n", | 
|  | 205 | +    "    print(curr_path)\n", | 
|  | 206 | +    "    yaml_tool_registrations: list[Tool] = parse_app_conf_tool_registrations(curr_path)\n", | 
|  | 207 | +    "    [print(toolReg) for toolReg in yaml_tool_registrations]\n", | 
|  | 208 | +    "\n", | 
|  | 209 | +    "\n", | 
|  | 210 | +    "await post_install()\n" | 
|  | 211 | +   ] | 
|  | 212 | +  } | 
|  | 213 | + ], | 
|  | 214 | + "metadata": { | 
|  | 215 | +  "kernelspec": { | 
|  | 216 | +   "display_name": ".venv", | 
|  | 217 | +   "language": "python", | 
|  | 218 | +   "name": "python3" | 
|  | 219 | +  }, | 
|  | 220 | +  "language_info": { | 
|  | 221 | +   "codemirror_mode": { | 
|  | 222 | +    "name": "ipython", | 
|  | 223 | +    "version": 3 | 
|  | 224 | +   }, | 
|  | 225 | +   "file_extension": ".py", | 
|  | 226 | +   "mimetype": "text/x-python", | 
|  | 227 | +   "name": "python", | 
|  | 228 | +   "nbconvert_exporter": "python", | 
|  | 229 | +   "pygments_lexer": "ipython3", | 
|  | 230 | +   "version": "3.13.7" | 
|  | 231 | +  } | 
|  | 232 | + }, | 
|  | 233 | + "nbformat": 4, | 
|  | 234 | + "nbformat_minor": 5 | 
|  | 235 | +} | 
0 commit comments