Skip to content

Commit fcae942

Browse files
committed
Finish up PoC
1 parent 5acec74 commit fcae942

File tree

4 files changed

+195
-113
lines changed

4 files changed

+195
-113
lines changed

splunklib/mcp/tools.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import configparser
2+
import os
3+
from dataclasses import asdict, dataclass, field
4+
from typing import Any, Literal
5+
6+
from fastmcp.client import Client
7+
from fastmcp.client.transports import PythonStdioTransport
8+
from mcp.types import Tool
9+
10+
11+
@dataclass
12+
class SplunkMeta:
13+
permissions: list[str] = field()
14+
tool_type: str = field(default="")
15+
schema_version: str = field(default="")
16+
execution_mode: str = field(default="")
17+
execution_endpoint: str = field(default="")
18+
19+
20+
@dataclass
21+
class McpInputOutputSchema:
22+
type: Literal["object"] = "object"
23+
properties: dict[str, Any] = field(default_factory=lambda: {}) # pyright: ignore[reportExplicitAny]
24+
required: list[str] = field(default_factory=lambda: [])
25+
26+
27+
tool_reg_prefix = "app:mcp_tool"
28+
29+
30+
def filter_sections(section_name: str) -> bool:
31+
return section_name.startswith(tool_reg_prefix)
32+
33+
34+
def match_input_schema(input: Literal["query_string"] | Literal["other"]):
35+
"""Gets super messy :("""
36+
match input:
37+
case "query_string":
38+
return {
39+
"type": "object",
40+
"properties": {
41+
"query_string": {
42+
"type": "string",
43+
"description": "SPL2 query string",
44+
}
45+
},
46+
}
47+
case _:
48+
raise NotImplementedError("We don't know what to put here lol")
49+
50+
51+
def parse_ai_conf(file_path: str) -> list[Tool]:
52+
config = configparser.ConfigParser()
53+
all_sections_len = config.read(file_path)
54+
if len(all_sections_len) == 0:
55+
return []
56+
57+
tool_reg_sections: list[str] = list(filter(filter_sections, config.sections()))
58+
if len(tool_reg_sections) == 0:
59+
return []
60+
61+
ini_tools: list[Tool] = []
62+
for reg_section in tool_reg_sections:
63+
reg_section_data = config[reg_section]
64+
65+
name: str = reg_section.split(":")[2]
66+
description = reg_section_data["description"]
67+
# https://modelcontextprotocol.io/specification/2025-06-18/schema#tool
68+
inputSchema = McpInputOutputSchema(properties={}, required=[])
69+
outputSchema = McpInputOutputSchema(properties={}, required=[])
70+
meta = SplunkMeta(
71+
permissions=[
72+
perm.strip()
73+
for perm in reg_section_data["permissions"].strip().split(",")
74+
],
75+
tool_type="search",
76+
schema_version=reg_section_data["schema_version"].strip(),
77+
)
78+
79+
ini_tool = Tool(
80+
name=name,
81+
description=description,
82+
inputSchema=asdict(inputSchema),
83+
outputSchema=asdict(outputSchema),
84+
_meta=asdict(meta),
85+
)
86+
ini_tools.append(ini_tool)
87+
88+
return ini_tools
89+
90+
91+
async def get_tools(server_path: str):
92+
mcp_client = Client(server_path)
93+
94+
tools = []
95+
async with mcp_client:
96+
tools = await mcp_client.list_tools()
97+
98+
# TODO: Get registrations from ai.conf
99+
# curr_path = os.path.join(os.getcwd(), "..", "default", "app.conf")
100+
# yaml_tool_registrations: list[Tool] = parse_ai_conf(curr_path)
101+
102+
return tools
103+
104+
105+
async def register_tools_from(file_paths: list[str]) -> None:
106+
"""TODO
107+
1. `POST /tools` with MCP payload
108+
2.
109+
"""
110+
print(file_paths)

tests/system/test_apps/mcp_enabled_app/bin/mcp_enabled_app.ipynb

Lines changed: 17 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,23 @@
1010
"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",
1111
"\n",
1212
"- 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"
13+
" - [ ] Capture e.g. tool_name, description, inputs, outputs\n"
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"id": "18ab5550",
20+
"metadata": {},
21+
"outputs": [],
22+
"source": []
23+
},
24+
{
25+
"cell_type": "markdown",
26+
"id": "363201e8",
27+
"metadata": {},
28+
"source": [
29+
"- MCP JSONSchema can (and most probably should) be used for tool registration in Splunk"
1530
]
1631
},
1732
{
@@ -98,117 +113,6 @@
98113
" - [ ] Call MCP registry with credentials (from App Manager)\n",
99114
" - [ ] Log success/fail to MCP audit\n"
100115
]
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-
]
212116
}
213117
],
214118
"metadata": {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from splunklib.mcp import tools
2+
3+
4+
async def post_install(splunk_url: str, auth_token: str) -> None:
5+
# TODO: Implement
6+
try:
7+
await tools.register_tools_from(["./tools.py", "../local/ai.conf"])
8+
except Exception as e:
9+
print(e)
10+
raise e
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import asyncio
2+
import json
3+
from dataclasses import asdict
4+
5+
from fastmcp import FastMCP
6+
7+
from splunklib import client
8+
from splunklib.mcp.tools import SplunkMeta
9+
from splunklib.results import JSONResultsReader
10+
11+
MCP_SERVER_HOST: str = "0.0.0.0"
12+
MCP_SERVER_PORT: int = 2137
13+
14+
MCP_SERVER_NAME: str = "GeneratingCSC PoC Server"
15+
app_mcp_server = FastMCP(MCP_SERVER_NAME)
16+
17+
18+
@app_mcp_server.tool(
19+
description="""
20+
The `generatingcsc` command generates a specific number of records.
21+
22+
Example:
23+
``| generatingcsc count=4``
24+
Returns a 4 records having text 'Test Event'.
25+
""",
26+
meta=asdict(
27+
SplunkMeta(
28+
permissions=["role:search_admin", "role:aws_analyst"],
29+
tool_type="search",
30+
schema_version="1.0",
31+
execution_endpoint="",
32+
execution_mode="",
33+
)
34+
),
35+
enabled=True,
36+
)
37+
def generating_csc(count: int = 10) -> list[str]:
38+
service = client.connect(
39+
scheme="https",
40+
host="localhost",
41+
port="8089",
42+
username="admin",
43+
password="changed!",
44+
autologin=True,
45+
)
46+
stream = service.jobs.oneshot(f"| generatingcsc count={count}", output_mode="json")
47+
results = JSONResultsReader(stream)
48+
49+
quuuuuux = [json.dumps(r) for r in list(results)]
50+
print(quuuuuux)
51+
return quuuuuux
52+
53+
54+
if __name__ == "__main__":
55+
asyncio.run(
56+
# app_mcp_server.run_streamable_http_async(MCP_SERVER_HOST, port=MCP_SERVER_PORT)
57+
app_mcp_server.run_stdio_async(False)
58+
)

0 commit comments

Comments
 (0)