blob: 7d64f525b2b62bb64a2903043b22406acd63e0cf [file]
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
from __future__ import annotations
import logging
from mcp.types import TextContent
from iotdb_mcp_server.config import Config
from iotdb_mcp_server.result_store import result_store_from_export_path
from iotdb_mcp_server.services.json_response import payload_response
from iotdb_mcp_server.services.target_selection import select_target_config
def _cursor_to_offset(cursor: str | None, offset: int | None) -> int:
if offset is not None:
return max(0, int(offset))
if cursor is None or str(cursor).strip() == "":
return 0
cleaned = str(cursor).strip()
if not cleaned.isdigit():
raise ValueError("cursor must be a non-negative integer row offset.")
return int(cleaned)
def _csv_text(columns: list[str], rows: list[str]) -> str:
return "\n".join([",".join(columns)] + rows)
def register_result_store_tools(
mcp,
config: Config,
logger: logging.Logger,
) -> None:
"""Register tools for paging file-backed MCP result sets."""
@mcp.tool()
async def read_result_page(
result_id: str,
cursor: str | None = None,
offset: int | None = None,
limit: int | None = None,
owner_session_id: str | None = None,
target_id: str | None = None,
target: dict[str, object] | None = None,
) -> list[TextContent]:
"""Read one page from a previously returned ResultStore result_id.
Pass the same target_id used by the original query when multiple IoTDB
targets are registered. cursor is the next_cursor returned by a prior
page; offset can be used for random access and takes precedence.
"""
selected_config = select_target_config(
config, target_id=target_id, target=target
)
store = result_store_from_export_path(selected_config.export_path)
try:
payload = store.read_page(
result_id,
offset=_cursor_to_offset(cursor, offset),
limit=limit,
owner_session_id=owner_session_id,
)
payload["target_id"] = selected_config.target_id
payload["text"] = _csv_text(payload["columns"], payload["rows"])
payload["inline_row_count"] = payload["returned_rows"]
payload["inline_truncated"] = payload["has_more"]
return payload_response(
"read_result_page",
payload,
message="Result page loaded from ResultStore.",
)
except Exception as e:
logger.error(f"Failed to read ResultStore page: {str(e)}")
raise
@mcp.tool()
async def read_result_pages(
pages: list[dict[str, object]],
owner_session_id: str | None = None,
default_limit: int | None = None,
max_pages: int | None = None,
max_total_rows: int | None = None,
continue_on_error: bool = True,
target_id: str | None = None,
target: dict[str, object] | None = None,
) -> list[TextContent]:
"""Read multiple ResultStore pages in one MCP call.
Each page request is an object with result_id plus optional cursor,
offset, limit, and owner_session_id. offset takes precedence over
cursor. owner_session_id on an item takes precedence over the shared
owner_session_id argument.
"""
selected_config = select_target_config(
config, target_id=target_id, target=target
)
store = result_store_from_export_path(selected_config.export_path)
try:
payload = store.read_pages(
pages,
owner_session_id=owner_session_id,
default_limit=default_limit,
max_pages=max_pages,
max_total_rows=max_total_rows,
continue_on_error=continue_on_error,
)
payload["target_id"] = selected_config.target_id
for page in payload["pages"]:
if page.get("status") != "success":
continue
page["text"] = _csv_text(page["columns"], page["rows"])
page["inline_row_count"] = page["returned_rows"]
page["inline_truncated"] = page["has_more"]
return payload_response(
"read_result_pages",
payload,
message="Result pages loaded from ResultStore.",
)
except Exception as e:
logger.error(f"Failed to read ResultStore pages: {str(e)}")
raise
@mcp.tool()
async def list_result_store(
owner_session_id: str | None = None,
all_sessions: bool = False,
limit: int = 50,
target_id: str | None = None,
target: dict[str, object] | None = None,
) -> list[TextContent]:
"""List ResultStore entries for the current session by default."""
selected_config = select_target_config(
config, target_id=target_id, target=target
)
store = result_store_from_export_path(selected_config.export_path)
try:
payload = store.list_results(
owner_session_id=owner_session_id,
all_sessions=all_sessions,
limit=limit,
)
payload["target_id"] = selected_config.target_id
return payload_response(
"list_result_store",
payload,
message="ResultStore entries listed.",
)
except Exception as e:
logger.error(f"Failed to list ResultStore entries: {str(e)}")
raise
@mcp.tool()
async def cleanup_result_store(
owner_session_id: str | None = None,
all_sessions: bool = False,
ttl_seconds: int | None = None,
target_id: str | None = None,
target: dict[str, object] | None = None,
) -> list[TextContent]:
"""Run TTL and quota cleanup for ResultStore entries."""
selected_config = select_target_config(
config, target_id=target_id, target=target
)
store = result_store_from_export_path(selected_config.export_path)
try:
payload = store.cleanup(
owner_session_id=owner_session_id,
all_sessions=all_sessions,
ttl_seconds=ttl_seconds,
)
payload["target_id"] = selected_config.target_id
return payload_response(
"cleanup_result_store",
payload,
message="ResultStore cleanup completed.",
)
except Exception as e:
logger.error(f"Failed to cleanup ResultStore: {str(e)}")
raise
@mcp.tool()
async def delete_result(
result_id: str,
owner_session_id: str | None = None,
all_sessions: bool = False,
target_id: str | None = None,
target: dict[str, object] | None = None,
) -> list[TextContent]:
"""Delete one cached ResultStore result."""
selected_config = select_target_config(
config, target_id=target_id, target=target
)
store = result_store_from_export_path(selected_config.export_path)
try:
payload = store.delete_result(
result_id,
owner_session_id=owner_session_id,
all_sessions=all_sessions,
)
payload["target_id"] = selected_config.target_id
return payload_response(
"delete_result",
payload,
message="ResultStore result deleted.",
)
except Exception as e:
logger.error(f"Failed to delete ResultStore result: {str(e)}")
raise