IoTDB MCP Services

This directory contains tool modules registered by src/iotdb_mcp_server/server.py.

Registration Overview

Current registration order:

  1. register_target_tools
  2. register_runtime_policy_tools
  3. register_result_store_tools
  4. register_query_tools
  5. register_sql_driver_tools
  6. register_metadata_tools
  7. register_explain_tools
  8. register_database_tools
  9. register_timeseries_tools
  10. register_ttl_tools
  11. register_table_tools
  12. register_udf_tools
  13. register_write_tools
  14. register_model_tools

Tool Matrix

query.py

  • Tree dialect:
    • select_query(query_sql, max_inline_rows=None, page_size_rows=None)
    • export_query(query_sql, format="csv", filename=None)
  • Table dialect:
    • read_query(query_sql, max_inline_rows=None, page_size_rows=None)
    • export_table_query(query_sql, format="csv", filename=None)

Read-query tools stream rows into ResultStore and return only a preview by default. Use the returned result_id and next_cursor with read_result_page.

results.py

  • read_result_page(result_id, cursor=None, offset=None, limit=None, owner_session_id=None, target_id=None)
  • read_result_pages(pages, owner_session_id=None, default_limit=None, max_pages=None, max_total_rows=None, continue_on_error=True, target_id=None)
  • list_result_store(owner_session_id=None, all_sessions=False, limit=50, target_id=None)
  • cleanup_result_store(owner_session_id=None, all_sessions=False, ttl_seconds=None, target_id=None)
  • delete_result(result_id, owner_session_id=None, all_sessions=False, target_id=None)

Loads a bounded page from a file-backed ResultStore result. Pass the same target_id as the original query when multiple IoTDB targets are registered. When the TimeSeek Codex plugin hook is active, owner_session_id is injected from the Codex session. Standalone MCP usage can pass owner_session_id explicitly or set TIMESEEK_MCP_SESSION_ID; otherwise the ResultStore owner is standalone.

ResultStore cleanup is finite and session-aware. Each write runs lightweight cleanup for the current owner and then enforces global limits. Entries expire by last_accessed_at, and quota pressure evicts least-recently-accessed results. If a single new result is larger than the byte quota, it is retained so the caller can still page through the result that was just returned.

read_result_pages batches multiple page requests in one MCP call. Each item accepts result_id plus optional cursor, offset, limit, and owner_session_id. The default per-call limits are 32 pages and 10000 returned rows; override them with tool arguments or TIMESEEK_RESULT_MAX_BATCH_PAGES and TIMESEEK_RESULT_MAX_BATCH_READ_ROWS.

sql_driver.py

  • Both dialects:
    • sql_driver_policy()
    • inspect_sql_permission(sql)
    • sql_executor_batch(sqls=None, sql_template=None, param_sets=None, max_concurrency=4, worker_pool_size=None, per_item_timeout_ms=None, batch_timeout_ms=None, max_result_rows_per_item=None, max_result_bytes_per_item=None, max_batch_result_rows=None, max_batch_result_bytes=None, continue_on_error=True, max_inline_rows=5, page_size_rows=None)
    • sql_execute(sql, confirm_destructive=False, max_inline_rows=None, page_size_rows=None)

Features:

  • Statement whitelist by category: readonly / ddl / full
  • Advisory permission metadata via IOTDB_SQL_DRIVER_MODE
  • SQL inspection returns required_permission, destructive, risk_level, approval_required, and the confirmation parameter without executing SQL
  • Destructive SQL confirmation policy
  • sql_executor_batch executes only readonly single statements. It accepts either explicit sqls=[...] or a repeated sql_template plus param_sets. Template placeholders are {{name}} for SQL literals, {{name:path}} for IoTDB paths, and {{name:identifier}} for SQL identifiers.
  • Batch results are per-statement ResultStore entries. The batch response returns status, result_id, row counts, a small preview, and paging metadata; use read_result_page to fetch full result pages.
  • Batch execution controls include a bounded thread worker pool, per-item and whole-batch wait timeouts, and per-item / whole-batch result row and byte quotas. Defaults: 4 concurrency, 60000 ms per item, 300000 ms per batch, 10000 rows and 16 MiB per item, 100000 rows and 64 MiB per batch.
  • Tree CQ enabled in whitelist:
    • Readonly: SHOW CONTINUOUS QUERIES, SHOW CQS
    • DDL: CREATE CONTINUOUS QUERY / CREATE CQ, DROP CONTINUOUS QUERY / DROP CQ
  • Downsampling execution path:
    • ad-hoc: SELECT ... GROUP BY ([start, end), interval[, slidingStep])
    • scheduled: CQ with INTO + optional RESAMPLE clause

runtime_policy.py

  • Session scoped permission controls:
    • get_iotdb_session_policy()
    • set_iotdb_session_policy(policy=None, preset=None, replace=False)
    • reset_iotdb_session_policy(keys=None)
  • Presets:
    • full: advisory signal for read, DDL, and DML-capable workflows
    • ddl: advisory signal for read and DDL-capable workflows
    • readonly: advisory signal for read-only workflows
    • In strict mode only, these presets become hard execution gates
  • Effective policy priority:
    • session overlay set by MCP tool
    • dynamically read .mcp.json
    • process environment
    • built-in full-permission defaults

metadata.py

  • Tree dialect:
    • metadata_query(query_sql)
    • list_timeseries(path="root.**")
    • list_devices(path="root.**")
    • list_child_paths(path)
    • list_child_nodes(path)
    • count_timeseries(path="root.**")
    • count_devices(path="root.**")
    • count_nodes(path="root")
  • Table dialect:
    • metadata_query(query_sql)
    • list_tables()
    • describe_table(table_name, details=True)

Metadata result sets also use ResultStore, which keeps large SHOW TIMESERIES or SHOW DEVICES responses bounded.

explain.py

  • Both dialects:
    • explain_query(query_sql, analyze=False)

database.py

  • Tree dialect:
    • list_databases(details=False)
    • create_database(database)
    • drop_database(database, confirm=False)
  • Table dialect:
    • list_databases(details=False)
    • create_database(database, if_not_exists=True)
    • drop_database(database, if_exists=True, confirm=False)
    • use_database(database)

timeseries.py (tree only)

  • create_timeseries_ddl(ddl_sql)
  • create_timeseries_batch_ddl(ddl_list, continue_on_error=True, skip_if_exists=True, max_statements=1000)
  • alter_timeseries_ddl(ddl_sql)
  • drop_timeseries_ddl(ddl_sql, confirm=False)

ttl.py (tree only)

  • ttl_command(ttl_sql, confirm_unset=False)
  • ttl_query(ttl_sql)

table.py (table only)

  • create_table_ddl(ddl_sql)
  • alter_table_ddl(ddl_sql)
  • drop_table_ddl(ddl_sql, confirm=False)

udf.py

  • Both dialects:
    • list_udf_functions()
    • prepare_udf_query(function_name, expressions, table_name=None, from_path=None, attributes=None, where_clause=None, limit=None, alias=None, align_by_device=False)
    • execute_udf_query(function_name, expressions, table_name=None, from_path=None, attributes=None, where_clause=None, limit=None, alias=None, align_by_device=False, max_inline_rows=None, page_size_rows=None)
    • export_udf_query(function_name, expressions, table_name=None, from_path=None, attributes=None, where_clause=None, limit=None, alias=None, align_by_device=False, format="csv", filename=None)

Features:

  • SHOW FUNCTIONS discovery through list_udf_functions
  • Conservative SQL generation for UDF calls instead of accepting raw SQL
  • Table dialect shape: SELECT UDF(column, "k"="v") FROM table ...
  • Tree dialect shape: SELECT UDF(measurement, "k"="v") FROM root.sg.d1 ...
  • Read-only execution only; DDL/DML keywords, semicolons, and comments are rejected in expressions and where_clause
  • ResultStore-backed previews for inline execution and export support for large UDF outputs

write.py

  • Tree dialect:
    • write_query(write_sql, confirm_delete=False) (INSERT/DELETE)
  • Table dialect:
    • write_query(write_sql, confirm_delete=False) (INSERT/UPDATE/DELETE)

model.py

  • Both dialects:
    • model_query(model_sql)
    • model_command(model_sql, confirm_destructive=False)
  • Tree dialect:
    • prepare_model_inference_request(model_id, input_sql, output_length=96, generate_time=False)
    • model_inference(inference_sql)

json_response.py

  • Shared response wrapper and parser:
    • JsonParser.check_format(obj)
    • JsonParser.parse(text)
    • payload_response(tool, payload, message=None)
    • text_payload_response(tool, text, message=None)
    • csv_payload_response(tool, columns, rows, message=None)
    • sql_success_response(tool, sql)

Tool Response JSON Envelope

All tools now return TextContent.text as a JSON envelope instead of plain text.

Envelope format:

{
  "version": "1.0",
  "tool": "tool_name",
  "ok": true,
  "timestamp": "2026-02-26T00:00:00+00:00",
  "payload": {},
  "message": "optional"
}

Payload conventions:

  • Query-like tools: payload.format="csv" with columns, preview rows, preview text, row_count, inline_row_count, inline_truncated, and ResultStore fields when persisted
  • Text-like tools: payload.format="text" with text
  • SQL success tools: payload.sql + payload.result="success"

For ResultStore-backed responses:

{
  "format": "csv",
  "columns": ["Time", "root.sg.d.s0"],
  "rows": ["0,1.0"],
  "text": "Time,root.sg.d.s0\n0,1.0",
  "row_count": 100000,
  "inline_row_count": 1,
  "inline_truncated": true,
  "result_id": "res_20260704T000000Z_0123456789abcdef",
  "owner_session_id": "ses_abc123",
  "next_cursor": "1",
  "result_store": {
    "type": "file",
    "page_tool": "read_result_page",
    "batch_page_tool": "read_result_pages",
    "owner_session_id": "ses_abc123",
    "page_size_rows": 500,
    "shard_count": 10
  }
}

Environment knobs:

  • TIMESEEK_RESULT_PREVIEW_ROWS default 50
  • TIMESEEK_RESULT_PAGE_SIZE_ROWS default 500
  • TIMESEEK_RESULT_MAX_PAGE_ROWS default 5000
  • TIMESEEK_RESULT_SHARD_ROWS default 10000
  • TIMESEEK_RESULT_SHARD_BYTES default 8388608
  • TIMESEEK_RESULT_TTL_SECONDS default 86400; set 0 to disable TTL expiry
  • TIMESEEK_RESULT_MAX_SESSION_RESULTS default 100; set 0 to disable
  • TIMESEEK_RESULT_MAX_SESSION_BYTES default 536870912; set 0 to disable
  • TIMESEEK_RESULT_MAX_GLOBAL_RESULTS default 1000; set 0 to disable
  • TIMESEEK_RESULT_MAX_GLOBAL_BYTES default 2147483648; set 0 to disable
  • TIMESEEK_MCP_SESSION_ID optional standalone ResultStore owner fallback
  • IOTDB_SQL_EXECUTOR_BATCH_MAX_STATEMENTS default 64
  • IOTDB_SQL_EXECUTOR_BATCH_MAX_CONCURRENCY default 16

Parser check:

  • Wrapper output is validated by JsonParser.check_format(...)
  • Serialized text is parsed again by JsonParser.parse(...) before returning
  • Invalid envelope shape raises ValueError

Security and Permission Model

IoTDB MCP permissions default to host-agent approval flow. The MCP server reports required permission and risk; Codex, Claude Code, or OpenCode owns user approval through its normal question/approval mechanism. This keeps MCP policy aligned with the host agent system instead of turning .mcp.json into a hard blocker.

Default mode is advisory. In advisory mode:

  • IOTDB_SQL_DRIVER_MODE is a prompt-layer signal, not an execution blocker.
  • IOTDB_ENABLE_* and *_ALLOWED_USERS are prompt-layer/session settings, not hard gates.
  • Destructive operations still require explicit tool confirmation flags, such as confirm_destructive=true, after the host agent has obtained approval.

Strict hard gating is available only when explicitly enabled:

TIMESEEK_MCP_PERMISSION_ENFORCEMENT=strict
# or
IOTDB_STRICT_PERMISSION_ENFORCEMENT=true

Policy can be changed at runtime without restarting the MCP server:

{
  "preset": "readonly",
  "replace": true
}

or:

{
  "policy": {
    "IOTDB_SQL_DRIVER_MODE": "full",
    "IOTDB_ENABLE_WRITE_DML": "true"
  }
}

Per-module advisory keys

  • database.py
    • IOTDB_ENABLE_DATABASE_DDL
    • IOTDB_DATABASE_DDL_ALLOWED_USERS
    • IOTDB_REQUIRE_DROP_CONFIRM
  • timeseries.py
    • IOTDB_ENABLE_TIMESERIES_DDL
    • IOTDB_TIMESERIES_DDL_ALLOWED_USERS
    • IOTDB_REQUIRE_TIMESERIES_DROP_CONFIRM
  • table.py
    • IOTDB_ENABLE_TABLE_DDL
    • IOTDB_TABLE_DDL_ALLOWED_USERS
    • IOTDB_REQUIRE_TABLE_DROP_CONFIRM
  • ttl.py
    • IOTDB_ENABLE_TTL_SQL
    • IOTDB_TTL_ALLOWED_USERS
    • IOTDB_REQUIRE_TTL_UNSET_CONFIRM
  • write.py
    • IOTDB_ENABLE_WRITE_DML
    • IOTDB_WRITE_ALLOWED_USERS
    • IOTDB_REQUIRE_DELETE_CONFIRM
  • model.py
    • IOTDB_ENABLE_MODEL_MANAGEMENT
    • IOTDB_MODEL_ALLOWED_USERS
    • IOTDB_REQUIRE_MODEL_DESTRUCTIVE_CONFIRM
    • model_inference is registered only for tree dialect and accepts CALL INFERENCE(...) SQL.
  • metadata.py
    • IOTDB_ENABLE_METADATA_QUERY
    • IOTDB_METADATA_ALLOWED_USERS
  • sql_driver.py
    • IOTDB_ENABLE_SQL_DRIVER
    • IOTDB_SQL_DRIVER_ALLOWED_USERS
    • IOTDB_SQL_DRIVER_MODE (readonly / ddl / full)
    • IOTDB_SQL_DRIVER_REQUIRE_DESTRUCTIVE_CONFIRM
    • IOTDB_SQL_DRIVER_EXTRA_READONLY_PREFIXES
    • IOTDB_SQL_DRIVER_EXTRA_DDL_PREFIXES
    • IOTDB_SQL_DRIVER_EXTRA_FULL_PREFIXES

Minimal Enablement Example

# Dialect
IOTDB_SQL_DIALECT=tree

# Keep generic driver available in full mode
IOTDB_ENABLE_SQL_DRIVER=true
IOTDB_SQL_DRIVER_MODE=full
IOTDB_SQL_DRIVER_ALLOWED_USERS=*

# Enable specialized tools
IOTDB_ENABLE_METADATA_QUERY=true
IOTDB_ENABLE_DATABASE_DDL=true
IOTDB_ENABLE_TIMESERIES_DDL=true
IOTDB_ENABLE_TTL_SQL=true
IOTDB_ENABLE_WRITE_DML=true
IOTDB_ENABLE_MODEL_MANAGEMENT=true

Notes

  • All tools enforce single-statement execution (; separated multi-statement is rejected).
  • Tool availability depends on sql_dialect (tree or table).
  • For production hard-block behavior, set TIMESEEK_MCP_PERMISSION_ENFORCEMENT=strict.
  • Keep destructive confirm flags enabled unless an external approval layer provides equivalent per-action confirmation.