blob: 27d1dad063ff602ee923aa179dbbfa0a4567b554 [file] [view]
# 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:
```json
{
"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:
```json
{
"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:
```bash
TIMESEEK_MCP_PERMISSION_ENFORCEMENT=strict
# or
IOTDB_STRICT_PERMISSION_ENFORCEMENT=true
```
Policy can be changed at runtime without restarting the MCP server:
```json
{
"preset": "readonly",
"replace": true
}
```
or:
```json
{
"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
```bash
# 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.