Skip to content
Draft
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
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ services:
- "./tests/system/test_apps/generating_app:/opt/splunk/etc/apps/generating_app"
- "./tests/system/test_apps/reporting_app:/opt/splunk/etc/apps/reporting_app"
- "./tests/system/test_apps/streaming_app:/opt/splunk/etc/apps/streaming_app"
- "./tests/system/test_apps/modularinput_app:/opt/splunk/etc/apps/modularinput_app"
- "./tests/system/test_apps/mcp_enabled_app:/opt/splunk/etc/apps/mcp_enabled_app"
- "./splunklib:/opt/splunk/etc/apps/eventing_app/bin/splunklib"
- "./splunklib:/opt/splunk/etc/apps/generating_app/bin/splunklib"
- "./splunklib:/opt/splunk/etc/apps/reporting_app/bin/splunklib"
- "./splunklib:/opt/splunk/etc/apps/streaming_app/bin/splunklib"
- "./splunklib:/opt/splunk/etc/apps/modularinput_app/bin/splunklib"
- "./splunklib:/opt/splunk/etc/apps/mcp_enabled_app/bin/splunklib"
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@
# built documents.
#
# The short X.Y version.
version = splunklib.__version__
version = splunklib.__VERSION__
# The full version, including alpha/beta/rc tags.
release = splunklib.__version__
release = splunklib.__VERSION__

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
15 changes: 11 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ classifiers = [
]

dependencies = ["python-dotenv>=0.21.1"]
optional-dependencies = { compat = ["six>=1.17.0"] }
optional-dependencies = { compat = ["six>=1.17.0"], mcp = [
# "splunk-sdk-mcp>=0.0.1",
] }

[dependency-groups]
build = ["build>=1.1.1", "twine>=4.0.2"]
Expand All @@ -47,14 +49,19 @@ dev = [
]

[build-system]
requires = ["setuptools"]
requires = ["setuptools", "build", "twine"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = ["splunklib", "splunklib.modularinput", "splunklib.searchcommands"]
packages = [
"splunklib",
"splunklib.modularinput",
"splunklib.searchcommands",
"splunklib.mcp",
]

[tool.setuptools.dynamic]
version = { attr = "splunklib.__version__" }
version = { attr = "splunklib.__VERSION__" }

# https://docs.astral.sh/ruff/configuration/
[tool.ruff.lint]
Expand Down
24 changes: 15 additions & 9 deletions splunklib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.

"""Python library for Splunk."""
"""Splunk Software Development Kit for Python"""

import logging

Expand All @@ -23,14 +23,20 @@
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S %Z"


# To set the logging level of splunklib
# ex. To enable debug logs, call this method with parameter 'logging.DEBUG'
# default logging level is set to 'WARNING'
def setup_logging(
level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE_FORMAT
):
logging.basicConfig(level=level, format=log_format, datefmt=date_format)
level: int = logging.WARNING,
log_format: str = DEFAULT_LOG_FORMAT,
date_format: str = DEFAULT_DATE_FORMAT,
force: bool = False,
) -> None:
"""Enable logs from splunklib"""
logging.basicConfig(
level=level, format=log_format, datefmt=date_format, force=force
)


__version_info__ = (2, 2, 0, "alpha")
__version__ = ".".join(map(str, __version_info__))
setup_logging(level=logging.DEBUG, force=True)


__VERSION_SEMVER__ = (2, 2, 0, "alpha")
__VERSION__ = ".".join(map(str, __VERSION_SEMVER__))
24 changes: 14 additions & 10 deletions splunklib/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@
from contextlib import contextmanager
from datetime import datetime
from functools import wraps
from io import BytesIO
from urllib import parse
from http import client
from http.cookies import SimpleCookie
from io import BytesIO
from urllib import parse
from xml.etree.ElementTree import XML, ParseError
from .data import record
from . import __version__

from splunklib.data import Record

from . import __VERSION__
from .data import record

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -511,7 +513,7 @@ class Context:
:param headers: List of extra HTTP headers to send (optional).
:type headers: ``list`` of 2-tuples.
:param retries: Number of retries for each HTTP connection (optional, the default is 0).
NOTE: THIS MAY INCREASE THE NUMBER OF ROUNDTRIP CONNECTIONS
NOTE: THIS MAY INCREASE THE NUMBER OF ROUNDTRIP CONNECTIONS
TO THE SPLUNK SERVER AND BLOCK THE CURRENT THREAD WHILE RETRYING.
:type retries: ``int``
:param retryDelay: How long to wait between connection attempts if `retries` > 0 (optional, defaults to 10s).
Expand Down Expand Up @@ -653,7 +655,9 @@ def connect(self):

@_authentication
@_log_duration
def delete(self, path_segment, owner=None, app=None, sharing=None, **query):
def delete(
self, path_segment, owner=None, app=None, sharing=None, **query
) -> Record:
"""Performs a DELETE operation at the REST path segment with the given
namespace and query.

Expand Down Expand Up @@ -716,7 +720,7 @@ def delete(self, path_segment, owner=None, app=None, sharing=None, **query):
@_log_duration
def get(
self, path_segment, owner=None, app=None, headers=None, sharing=None, **query
):
) -> Record:
"""Performs a GET operation from the REST path segment with the given
namespace and query.

Expand Down Expand Up @@ -783,7 +787,7 @@ def get(
@_log_duration
def post(
self, path_segment, owner=None, app=None, sharing=None, headers=None, **query
):
) -> Record:
"""Performs a POST operation from the REST path segment with the given
namespace and query.

Expand Down Expand Up @@ -1357,7 +1361,7 @@ def get(self, url, headers=None, **kwargs):
url = url + UrlEncoded("?" + _encode(**kwargs), skip_encode=True)
return self.request(url, {"method": "GET", "headers": headers})

def post(self, url, headers=None, **kwargs):
def post(self, url, headers=None, **kwargs) -> Record:
"""Sends a POST request to a URL.

:param url: The URL.
Expand Down Expand Up @@ -1559,7 +1563,7 @@ def request(url, message, **kwargs):
body = message.get("body", "")
head = {
"Content-Length": str(len(body)),
"User-Agent": "splunk-sdk-python/%s" % __version__,
"User-Agent": f"splunk-sdk-python/{__VERSION__}",
"Accept": "*/*",
"Connection": "Close",
} # defaults
Expand Down
15 changes: 10 additions & 5 deletions splunklib/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
"""

import contextlib
import datetime
import json
import logging
import re
Expand All @@ -68,8 +67,9 @@
from time import sleep
from urllib import parse

from splunklib.data import Record

from . import data
from .data import record
from .binding import (
AuthenticationError,
Context,
Expand All @@ -80,6 +80,7 @@
_NoAuthenticationToken,
namespace,
)
from .data import record

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -808,7 +809,7 @@ class Endpoint:
:class:`Entity` (essentially HTTP GET and POST methods).
"""

def __init__(self, service, path):
def __init__(self, service: Service, path):
self.service = service
self.path = path

Expand All @@ -833,7 +834,9 @@ def get_api_version(self, path):

return api_version

def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
def get(
self, path_segment="", owner=None, app=None, sharing=None, **query
) -> Record:
"""Performs a GET operation on the path segment relative to this endpoint.

This method is named to match the HTTP method. This method makes at least
Expand Down Expand Up @@ -916,7 +919,9 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query):

return self.service.get(path, owner=owner, app=app, sharing=sharing, **query)

def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
def post(
self, path_segment="", owner=None, app=None, sharing=None, **query
) -> Record:
"""Performs a POST operation on the path segment relative to this endpoint.

This method is named to match the HTTP method. This method makes at least
Expand Down
3 changes: 2 additions & 1 deletion splunklib/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
format, which is the format used by most of the REST API.
"""

from typing import Any
from xml.etree.ElementTree import XML

__all__ = ["load", "record"]
Expand Down Expand Up @@ -201,7 +202,7 @@ def load_value(element, nametable=None):


# A generic utility that enables "dot" access to dicts
class Record(dict):
class Record(dict[Any, Any]): # pyright: ignore[reportExplicitAny]
"""This generic utility class enables dot access to members of a Python
dictionary.

Expand Down
14 changes: 14 additions & 0 deletions splunklib/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import logging

mcp_package_name = "splunklib.mcp"

# <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/>
try:
__import__(mcp_package_name)

except ModuleNotFoundError as mnfe:
logging.error("Tried to import splunk-sdk-mcp without installing int", mnfe)

raise ModuleNotFoundError(
"PLease install splunk-sdk-mcp package to use these features."
) from mnfe
19 changes: 19 additions & 0 deletions tests/system/test_apps/mcp_enabled_app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# splunk-mcp-poc

This example aspires to verify the points listed in an internal "POC - AI with Splunk Apps" doc

## Pre-requirements

- Python 3.10+
- `uv`
- A way of launching Jupyter notebooks

## Getting started

- Run `uv sync`
- `source .venv/bin/activate`
- Run the code blocks `bin/mcp_enabled_app.ipynb`

## TODO

- Research using server composition: <https://gofastmcp.com/servers/composition>
69 changes: 69 additions & 0 deletions tests/system/test_apps/mcp_enabled_app/bin/mcp_enabled_app.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "f7f79ace",
"metadata": {},
"source": [
"# POC – MCP Tool Registration\n",
"\n",
"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",
"\n",
"- Develop @tool Decorator\n",
" - [ ] Capture e.g. tool_name, description, inputs, outputs\n",
"\n",
"- MCP JSONSchema can (and most probably should) be used for tool registration in Splunk\n",
"- execution_mode (external_http)\n",
" - Is this what MCP calls `transports`?\n",
"- execution_metadata (endpoint URL)\n",
" - Isn't that just `/execute_tool` or `tool/call`?"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "931ff98f",
"metadata": {},
"outputs": [],
"source": [
"from fastmcp.client import Client\n",
"\n",
"mcp_client = Client(\"tools.py\")\n",
"\n",
"\n",
"async def call_tool(tool_name: str, arguments: dict[str, int]):\n",
" result = None\n",
" async with mcp_client:\n",
" result = await mcp_client.call_tool(tool_name, arguments)\n",
"\n",
" return result\n",
"\n",
"\n",
"call_tool_result = await call_tool(\"generating_csc\", {\"count\": 10})\n",
"\n",
"[print(data) for data in call_tool_result.data]\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading