Skip to content

Commit bbdedea

Browse files
authored
Use services/mcp_token for retrieving auth token for MCP (#63)
Splunk MCP Server App stopped supporting normal splunk tokens. And requires calling this endpoint to retrieve the token for use in auth to the services/mcp endpoint. Additionally during testing I have noticed few timeouts during tool calls. The default MCP timeouts are higher, but since we override the default httpx client we do not get these, so lets increase these.
1 parent 661e0d9 commit bbdedea

2 files changed

Lines changed: 102 additions & 5 deletions

File tree

splunklib/ai/tools.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from mcp.types import Tool as MCPTool
2424
from pydantic import BaseModel
2525

26+
from splunklib.binding import HTTPError
2627
from splunklib.client import Service
2728
from splunklib.ai.registry import _map_logger_to_mcp_logging_level
2829

@@ -175,6 +176,11 @@ async def _connect_local_mcp(cfg: LocalCfg, logger: _MCPLoggingHandler | None =
175176
yield session
176177

177178

179+
# Based on streamable_http_client defaults, when http_client is usnet.
180+
_MCP_DEFAULT_TIMEOUT = 30.0 # General operations (seconds)
181+
_MCP_DEFAULT_SSE_READ_TIMEOUT = 300.0 # SSE streams - 5 minutes (seconds)
182+
183+
178184
@asynccontextmanager
179185
async def _connect_remote_mcp(cfg: RemoteCfg):
180186
async with streamable_http_client(
@@ -187,6 +193,9 @@ async def _connect_remote_mcp(cfg: RemoteCfg):
187193
auth=_MCPAuth(f"Bearer {cfg.token}"),
188194
verify=False,
189195
follow_redirects=True,
196+
timeout=httpx.Timeout(
197+
_MCP_DEFAULT_TIMEOUT, read=_MCP_DEFAULT_SSE_READ_TIMEOUT
198+
),
190199
),
191200
) as (read, write, _):
192201
async with ClientSession(read, write) as session:
@@ -357,6 +366,24 @@ class ResponseBody(BaseModel):
357366
return body.entry[0].content.token
358367

359368

369+
def _get_mcp_token(service: Service) -> str | None:
370+
try:
371+
res = service.get(
372+
path_segment="mcp_token",
373+
username=_get_splunk_username(service),
374+
output_mode="json",
375+
)
376+
except HTTPError as e:
377+
if e.status == 404:
378+
return None
379+
raise
380+
381+
class ResponseBody(BaseModel):
382+
token: str
383+
384+
return ResponseBody.model_validate_json(str(res.body)).token
385+
386+
360387
async def _load_tools(cfg: LocalCfg | RemoteCfg, logger: logging.Logger) -> list[Tool]:
361388
tools = await _list_all_tools(cfg)
362389
return [_convert_mcp_tool(logger, cfg, tool) for tool in tools]
@@ -376,13 +403,16 @@ async def load_mcp_tools(
376403
mcp_url = f"{management_url}/services/mcp"
377404
token = await asyncio.to_thread(lambda: _get_splunk_token_for_mcp(service))
378405

379-
# Load remote MCP tools, only if the MCP server App is available.
380-
client = httpx.AsyncClient(auth=_MCPAuth(f"Bearer {token}"), verify=False)
381-
res = await client.get(mcp_url)
382-
if res.status_code != 404:
406+
mcp_token = await asyncio.to_thread(lambda: _get_mcp_token(service))
407+
if mcp_token is not None:
383408
logger.debug("Splunk MCP Server App detected - loading remote tools")
384409
remote_tools = await _load_tools(
385-
RemoteCfg(mcp_url=mcp_url, token=token, app_id=app_id, trace_id=trace_id),
410+
RemoteCfg(
411+
mcp_url=mcp_url,
412+
token=mcp_token,
413+
app_id=app_id,
414+
trace_id=trace_id,
415+
),
386416
logger,
387417
)
388418
logger.debug(

tests/integration/ai/test_agent_mcp_tools.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import contextlib
33
from dataclasses import asdict, dataclass
4+
import logging
45
import os
56
import socket
67
from typing import Annotated
@@ -197,6 +198,13 @@ class ResponseBody(BaseModel):
197198
)
198199

199200

201+
async def mcp_token_handler(_: Request) -> Response:
202+
return JSONResponse(
203+
content={"token": AUTH_TOKEN},
204+
status_code=200,
205+
)
206+
207+
200208
class TestRemoteTools(AITestCase):
201209
@patch(
202210
"splunklib.ai.agent._testing_local_tools_path",
@@ -263,6 +271,11 @@ async def dispatch(self, request: Request, call_next):
263271
Starlette(
264272
routes=[
265273
Mount("/services/mcp", app=mcp.streamable_http_app()),
274+
Route(
275+
"/services/mcp_token",
276+
mcp_token_handler,
277+
methods=["GET"],
278+
),
266279
Route(
267280
"/services/authorization/tokens",
268281
tokens_handler,
@@ -402,6 +415,11 @@ async def lifespan(app: Starlette):
402415
Starlette(
403416
routes=[
404417
Mount("/services/mcp", app=mcp.streamable_http_app()),
418+
Route(
419+
"/services/mcp_token",
420+
mcp_token_handler,
421+
methods=["GET"],
422+
),
405423
Route(
406424
"/services/authorization/tokens",
407425
tokens_handler,
@@ -498,6 +516,11 @@ async def lifespan(app: Starlette):
498516
Starlette(
499517
routes=[
500518
Mount("/services/mcp", app=mcp.streamable_http_app()),
519+
Route(
520+
"/services/mcp_token",
521+
mcp_token_handler,
522+
methods=["GET"],
523+
),
501524
Route(
502525
"/services/authorization/tokens",
503526
tokens_handler,
@@ -551,6 +574,50 @@ async def lifespan(app: Starlette):
551574
response = result.messages[-1].content
552575
assert "31.5" in response, "Invalid LLM response"
553576

577+
@patch(
578+
"splunklib.ai.agent._testing_local_tools_path",
579+
os.path.join(
580+
os.path.dirname(__file__),
581+
"testdata",
582+
"non_existent.py",
583+
),
584+
)
585+
@patch("splunklib.ai.agent._testing_app_id", "app_id")
586+
@pytest.mark.asyncio
587+
async def test_splunk_mcp_server_app(self) -> None:
588+
# Skip if the langchain_openai package is not installed
589+
pytest.importorskip("langchain_openai")
590+
591+
# TODO: Remove this test once we have an E2E with Splunk MCP Server app.
592+
593+
self.skipTest("manual test")
594+
595+
logger = logging.getLogger("test")
596+
logger.setLevel(logging.DEBUG)
597+
598+
service = connect(
599+
port=8090,
600+
host="localhost",
601+
username="admin",
602+
password="",
603+
autologin=True,
604+
)
605+
606+
async with Agent(
607+
model=(await self.model()),
608+
system_prompt="You must use the available tools to perform requested operations",
609+
service=service,
610+
use_mcp_tools=True,
611+
logger=logger,
612+
) as agent:
613+
for tool in agent.tools:
614+
if tool.name == "splunk_get_indexes":
615+
result = await tool.func()
616+
assert len(result.structured_content["results"]) != 0
617+
return
618+
619+
assert False, "tool splunk_get_indexes not found"
620+
554621

555622
@contextlib.asynccontextmanager
556623
async def run_http_server(app: Starlette):

0 commit comments

Comments
 (0)