iflow-mcp_alaturqua-mcp-trino-python 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
src/config.py ADDED
@@ -0,0 +1,39 @@
1
+ """Configuration module for Trino connection settings."""
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+ import trino
7
+ from dotenv import load_dotenv
8
+
9
+
10
+ @dataclass
11
+ class TrinoConfig:
12
+ """Configuration class for Trino connection settings."""
13
+
14
+ host: str
15
+ port: int
16
+ user: str
17
+ catalog: str | None = None
18
+ schema: str | None = None
19
+ http_scheme: str = "http"
20
+ auth: trino.auth.BasicAuthentication | None = None
21
+ source: str = "mcp-trino-python"
22
+
23
+
24
+ def load_config() -> TrinoConfig:
25
+ """Load Trino configuration from environment variables."""
26
+ load_dotenv(override=True)
27
+
28
+ return TrinoConfig(
29
+ host=os.getenv("TRINO_HOST", "localhost"),
30
+ port=int(os.getenv("TRINO_PORT", "8080")),
31
+ user=os.getenv("TRINO_USER", os.getenv("USER", "trino")),
32
+ catalog=os.getenv("TRINO_CATALOG"),
33
+ schema=os.getenv("TRINO_SCHEMA"),
34
+ http_scheme=os.getenv("TRINO_HTTP_SCHEME", "http"),
35
+ auth=None
36
+ if os.getenv("TRINO_PASSWORD", None) is None
37
+ else trino.auth.BasicAuthentication(os.getenv("TRINO_USER", None), os.getenv("TRINO_PASSWORD", None)),
38
+ source="mcp-trino-python",
39
+ )
src/server.py ADDED
@@ -0,0 +1,617 @@
1
+ """Model Context Protocol server for Trino.
2
+
3
+ This module provides a Model Context Protocol (MCP) server that exposes Trino
4
+ functionality through resources and tools, with special support for Iceberg tables.
5
+ """
6
+
7
+ import os
8
+
9
+ from mcp.server.fastmcp import FastMCP
10
+ from mcp.server.fastmcp.prompts import base
11
+ from pydantic import Field
12
+
13
+ from config import load_config
14
+ from trino_client import TrinoClient
15
+
16
+ # Initialize the MCP server and Trino client
17
+ config = load_config()
18
+ client = TrinoClient(config)
19
+
20
+
21
+ # Initialize the MCP server with recommended settings for all transports
22
+ mcp = FastMCP(
23
+ name="Trino Explorer",
24
+ instructions="This Model Context Protocol (MCP) server provides access to Trino Query Engine.",
25
+ # HTTP transport settings (used by streamable-http and sse)
26
+ host=os.getenv("MCP_HOST", "127.0.0.1"),
27
+ port=int(os.getenv("MCP_PORT", "8000")),
28
+ # Recommended for production HTTP deployments
29
+ stateless_http=True,
30
+ json_response=True,
31
+ )
32
+
33
+
34
+ # Tools
35
+ @mcp.tool(description="List all available catalogs")
36
+ def show_catalogs() -> str:
37
+ """List all available catalogs."""
38
+ return client.list_catalogs()
39
+
40
+
41
+ @mcp.tool(description="List all schemas in a catalog")
42
+ def show_schemas(catalog: str = Field(description="The name of the catalog")) -> str:
43
+ """List all schemas in a catalog.
44
+
45
+ Args:
46
+ catalog: The name of the catalog
47
+
48
+ Returns:
49
+ str: List of schemas in the specified catalog
50
+ """
51
+ return client.list_schemas(catalog)
52
+
53
+
54
+ @mcp.tool(description="List all tables in a schema")
55
+ def show_tables(
56
+ catalog: str = Field(description="The name of the catalog"),
57
+ schema_name: str = Field(description="The name of the schema"),
58
+ ) -> str:
59
+ """List all tables in a schema.
60
+
61
+ Args:
62
+ catalog: The name of the catalog
63
+ schema_name: The name of the schema
64
+
65
+ Returns:
66
+ str: List of tables in the specified schema
67
+ """
68
+ return client.list_tables(catalog, schema_name)
69
+
70
+
71
+ @mcp.tool(description="Describe a table")
72
+ def describe_table(
73
+ catalog: str = Field(description="The catalog name"),
74
+ schema_name: str = Field(description="The schema name"),
75
+ table: str = Field(description="The name of the table"),
76
+ ) -> str:
77
+ """Describe a table.
78
+
79
+ Args:
80
+ catalog (str): The catalog name
81
+ schema_name (str): The schema name
82
+ table (str): The name of the table
83
+
84
+ Returns:
85
+ str: Table description in JSON format
86
+ """
87
+ return client.describe_table(catalog, schema_name, table)
88
+
89
+
90
+ @mcp.tool(description="Show the CREATE TABLE statement for a specific table")
91
+ def show_create_table(
92
+ catalog: str = Field(description="catalog name "),
93
+ schema_name: str = Field(description="schema name "),
94
+ table: str = Field(description="The name of the table"),
95
+ ) -> str:
96
+ """Show the CREATE TABLE statement for a table.
97
+
98
+ Args:
99
+ catalog: catalog name
100
+ schema_name: schema name
101
+ table: The name of the table
102
+
103
+ Returns:
104
+ str: The CREATE TABLE statement
105
+ """
106
+ return client.show_create_table(catalog, schema_name, table)
107
+
108
+
109
+ @mcp.tool(description="Show the CREATE VIEW statement for a specific view")
110
+ def show_create_view(
111
+ catalog: str = Field(description="catalog name "),
112
+ schema_name: str = Field(description="schema name "),
113
+ view: str = Field(description="The name of the view"),
114
+ ) -> str:
115
+ """Show the CREATE VIEW statement for a view.
116
+
117
+ Args:
118
+ catalog: catalog name
119
+ schema_name: schema name
120
+ view: The name of the view
121
+
122
+ Returns:
123
+ str: The CREATE VIEW statement
124
+ """
125
+ return client.show_create_view(catalog, schema_name, view)
126
+
127
+
128
+ @mcp.tool(description="Execute a SQL query and return results in a readable format")
129
+ def execute_query(query: str = Field(description="The SQL query to execute")) -> str:
130
+ """Execute a SQL query and return formatted results.
131
+
132
+ Args:
133
+ query: The SQL query to execute
134
+
135
+ Returns:
136
+ str: Query results formatted as a JSON string
137
+ """
138
+ return client.execute_query(query)
139
+
140
+
141
+ @mcp.tool(description="Optimize an Iceberg table's data files")
142
+ def optimize(
143
+ catalog: str = Field(description="catalog name "),
144
+ schema_name: str = Field(description="schema name "),
145
+ table: str = Field(description="The name of the table to optimize"),
146
+ ) -> str:
147
+ """Optimize an Iceberg table by compacting small files.
148
+
149
+ Args:
150
+ catalog: catalog name
151
+ schema_name: schema name
152
+ table: The name of the table to optimize
153
+
154
+ Returns:
155
+ str: Confirmation message
156
+ """
157
+ return client.optimize(catalog, schema_name, table)
158
+
159
+
160
+ @mcp.tool(description="Optimize manifest files for an Iceberg table")
161
+ def optimize_manifests(
162
+ catalog: str = Field(description="catalog name "),
163
+ schema_name: str = Field(description="schema name "),
164
+ table: str = Field(description="The name of the table"),
165
+ ) -> str:
166
+ """Optimize manifest files for an Iceberg table.
167
+
168
+ Args:
169
+ catalog: catalog name
170
+ schema_name: schema name
171
+ table: The name of the table
172
+
173
+ Returns:
174
+ str: Confirmation message
175
+ """
176
+ return client.optimize_manifests(catalog, schema_name, table)
177
+
178
+
179
+ @mcp.tool(description="Remove old snapshots from an Iceberg table")
180
+ def expire_snapshots(
181
+ catalog: str = Field(description="catalog name "),
182
+ schema_name: str = Field(description="schema name "),
183
+ retention_threshold: str = Field(
184
+ description="Age threshold for snapshot removal (e.g., '7d', '30d')", default="7d"
185
+ ),
186
+ table: str = Field(description="The name of the table"),
187
+ ) -> str:
188
+ """Remove old snapshots from an Iceberg table.
189
+
190
+ Args:
191
+ catalog: catalog name
192
+ schema_name: schema name
193
+ table: The name of the table
194
+ retention_threshold: Age threshold for snapshot removal (e.g., "7d", "30d")
195
+
196
+ Returns:
197
+ str: Confirmation message
198
+ """
199
+ return client.expire_snapshots(catalog, schema_name, table, retention_threshold)
200
+
201
+
202
+ @mcp.tool(description="Show statistics for a table")
203
+ def show_stats(
204
+ catalog: str = Field(description="catalog name "),
205
+ schema_name: str = Field(description="schema name "),
206
+ table: str = Field(description="The name of the table"),
207
+ ) -> str:
208
+ """Show statistics for a table.
209
+
210
+ Args:
211
+ catalog: catalog name
212
+ schema_name: schema name
213
+ table: The name of the table
214
+
215
+ Returns:
216
+ str: Table statistics in JSON format
217
+ """
218
+ return client.show_stats(catalog, schema_name, table)
219
+
220
+
221
+ @mcp.tool(name="show_query_history", description="Get the history of executed queries")
222
+ def show_query_history(
223
+ limit: int = Field(description="maximum number of history entries to return", default=None),
224
+ ) -> str:
225
+ """Get the history of executed queries.
226
+
227
+ Args:
228
+ limit: maximum number of history entries to return.
229
+ If None, returns all entries.
230
+
231
+ Returns:
232
+ str: JSON-formatted string containing query history.
233
+ """
234
+ return client.get_query_history(limit)
235
+
236
+
237
+ @mcp.tool(description="Show a hierarchical tree view of catalogs, schemas, and tables")
238
+ def show_catalog_tree() -> str:
239
+ """Get a hierarchical tree view showing the full structure of catalogs, schemas, and tables.
240
+
241
+ Returns:
242
+ str: A formatted string showing the catalog > schema > table hierarchy with visual indicators
243
+ """
244
+ return client.show_catalog_tree()
245
+
246
+
247
+ @mcp.tool(description="Show Iceberg table properties")
248
+ def show_table_properties(
249
+ catalog: str = Field(description="catalog name "),
250
+ schema_name: str = Field(description="schema name "),
251
+ table: str = Field(description="The name of the table"),
252
+ ) -> str:
253
+ """Show Iceberg table properties.
254
+
255
+ Args:
256
+ catalog: catalog name
257
+ schema_name: schema name
258
+ table: The name of the table
259
+
260
+ Returns:
261
+ str: JSON-formatted table properties
262
+ """
263
+ return client.show_table_properties(catalog, schema_name, table)
264
+
265
+
266
+ @mcp.tool(description="Show Iceberg table history/changelog")
267
+ def show_table_history(
268
+ catalog: str = Field(description="catalog name "),
269
+ schema_name: str = Field(description="schema name "),
270
+ table: str = Field(description="The name of the table"),
271
+ ) -> str:
272
+ """Show Iceberg table history/changelog.
273
+
274
+ The history contains:
275
+ - made_current_at: When snapshot became active
276
+ - snapshot_id: Identifier of the snapshot
277
+ - parent_id: Identifier of the parent snapshot
278
+ - is_current_ancestor: Whether snapshot is an ancestor of current
279
+
280
+ Args:
281
+ catalog: catalog name
282
+ schema_name: schema name
283
+ table: The name of the table
284
+
285
+ Returns:
286
+ str: JSON-formatted table history
287
+ """
288
+ return client.show_table_history(catalog, schema_name, table)
289
+
290
+
291
+ @mcp.tool(description="Show metadata for the table")
292
+ def show_metadata_log_entries(
293
+ catalog: str = Field(description="catalog name "),
294
+ schema_name: str = Field(description="schema name "),
295
+ table: str = Field(description="The name of the table"),
296
+ ) -> str:
297
+ """Show Iceberg table metadata log entries.
298
+
299
+ The metadata log contains:
300
+ - timestamp: When metadata was created
301
+ - file: Location of the metadata file
302
+ - latest_snapshot_id: ID of latest snapshot when metadata was updated
303
+ - latest_schema_id: ID of latest schema when metadata was updated
304
+ - latest_sequence_number: Data sequence number of metadata file
305
+
306
+ Args:
307
+ catalog: catalog name
308
+ schema_name: schema name
309
+ table: The name of the table
310
+
311
+ Returns:
312
+ str: JSON-formatted metadata log entries
313
+ """
314
+ return client.show_metadata_log_entries(catalog, schema_name, table)
315
+
316
+
317
+ @mcp.tool(description="Show Iceberg table snapshots")
318
+ def show_snapshots(
319
+ catalog: str = Field(description="catalog name "),
320
+ schema_name: str = Field(description="schema name "),
321
+ table: str = Field(description="The name of the table"),
322
+ ) -> str:
323
+ """Show Iceberg table snapshots.
324
+
325
+ The snapshots table contains:
326
+ - committed_at: When snapshot became active
327
+ - snapshot_id: Identifier for the snapshot
328
+ - parent_id: Identifier for the parent snapshot
329
+ - operation: Type of operation (append/replace/overwrite/delete)
330
+ - manifest_list: List of Avro manifest files
331
+ - summary: Summary of changes from previous snapshot
332
+
333
+ Args:
334
+ catalog: catalog name
335
+ schema_name: schema name
336
+ table: The name of the table
337
+
338
+ Returns:
339
+ str: JSON-formatted table snapshots
340
+ """
341
+ return client.show_snapshots(catalog, schema_name, table)
342
+
343
+
344
+ @mcp.tool(description="Show Iceberg table manifests")
345
+ def show_manifests(
346
+ catalog: str = Field(description="catalog name"),
347
+ schema_name: str = Field(description="schema name"),
348
+ table: str = Field(description="The name of the table"),
349
+ all_snapshots: bool = False,
350
+ ) -> str:
351
+ """Show Iceberg table manifests for current or all snapshots.
352
+
353
+ The manifests table contains:
354
+ - path: Manifest file location
355
+ - length: Manifest file length
356
+ - partition_spec_id: ID of partition spec used
357
+ - added_snapshot_id: ID of snapshot when manifest was added
358
+ - added_data_files_count: Number of data files with status ADDED
359
+ - added_rows_count: Total rows in ADDED files
360
+ - existing_data_files_count: Number of EXISTING files
361
+ - existing_rows_count: Total rows in EXISTING files
362
+ - deleted_data_files_count: Number of DELETED files
363
+ - deleted_rows_count: Total rows in DELETED files
364
+ - partition_summaries: Partition range metadata
365
+
366
+ Args:
367
+ catalog: catalog name
368
+ schema_name: schema name
369
+ table: The name of the table
370
+ all_snapshots: If True, show manifests from all snapshots
371
+
372
+ Returns:
373
+ str: JSON-formatted table manifests
374
+ """
375
+ return client.show_manifests(catalog, schema_name, table, all_snapshots)
376
+
377
+
378
+ @mcp.tool(description="Show Iceberg table partitions")
379
+ def show_partitions(
380
+ catalog: str = Field(description="catalog name "),
381
+ schema_name: str = Field(description="schema name "),
382
+ table: str = Field(description="The name of the table"),
383
+ ) -> str:
384
+ """Show Iceberg table partitions.
385
+
386
+ The partitions table contains:
387
+ - partition: Mapping of partition column names to values
388
+ - record_count: Number of records in partition
389
+ - file_count: Number of files in partition
390
+ - total_size: Total size of files in partition
391
+ - data: Partition range metadata with min/max values and null/nan counts
392
+
393
+ Args:
394
+ catalog: catalog name
395
+ schema_name: schema name
396
+ table: The name of the table
397
+
398
+ Returns:
399
+ str: JSON-formatted table partitions
400
+ """
401
+ return client.show_partitions(catalog, schema_name, table)
402
+
403
+
404
+ @mcp.tool(description="Show Iceberg table data files")
405
+ def show_files(
406
+ catalog: str = Field(description="catalog name "),
407
+ schema_name: str = Field(description="schema name "),
408
+ table: str = Field(description="The name of the table"),
409
+ ) -> str:
410
+ """Show Iceberg table data files in current snapshot.
411
+
412
+ The files table contains:
413
+ - content: Type of content (0=DATA, 1=POSITION_DELETES, 2=EQUALITY_DELETES)
414
+ - file_path: Data file location
415
+ - file_format: Format of the data file
416
+ - record_count: Number of records in file
417
+ - file_size_in_bytes: File size
418
+ - column_sizes: Column ID to size mapping
419
+ - value_counts: Column ID to value count mapping
420
+ - null_value_counts: Column ID to null count mapping
421
+ - nan_value_counts: Column ID to NaN count mapping
422
+ - lower_bounds: Column ID to lower bound mapping
423
+ - upper_bounds: Column ID to upper bound mapping
424
+ - key_metadata: Encryption key metadata
425
+ - split_offsets: Recommended split locations
426
+ - equality_ids: Field IDs for equality deletes
427
+
428
+ Args:
429
+ catalog: catalog name
430
+ schema_name: schema name
431
+ table: The name of the table
432
+
433
+ Returns:
434
+ str: JSON-formatted table files info
435
+ """
436
+ return client.show_files(catalog, schema_name, table)
437
+
438
+
439
+ @mcp.tool(description="Show Iceberg table manifest entries")
440
+ def show_entries(
441
+ catalog: str = Field(description="catalog name "),
442
+ schema_name: str = Field(description="schema name "),
443
+ table: str = Field(description="The name of the table"),
444
+ all_snapshots: bool = False,
445
+ ) -> str:
446
+ """Show Iceberg table manifest entries for current or all snapshots.
447
+
448
+ The entries table contains:
449
+ - status: Status of entry (0=EXISTING, 1=ADDED, 2=DELETED)
450
+ - snapshot_id: ID of the snapshot
451
+ - sequence_number: Data sequence number
452
+ - file_sequence_number: File sequence number
453
+ - data_file: File metadata including path, format, size etc
454
+ - readable_metrics: Human-readable file metrics
455
+
456
+ Args:
457
+ catalog: catalog name
458
+ schema_name: schema name
459
+ table: The name of the table
460
+ all_snapshots: If True, show entries from all snapshots
461
+
462
+ Returns:
463
+ str: JSON-formatted manifest entries
464
+ """
465
+ return client.show_entries(catalog, schema_name, table, all_snapshots)
466
+
467
+
468
+ @mcp.tool(description="Show Iceberg table references (branches and tags)")
469
+ def show_refs(
470
+ catalog: str = Field(description="catalog name "),
471
+ schema_name: str = Field(description="schema name "),
472
+ table: str = Field(description="The name of the table"),
473
+ ) -> str:
474
+ """Show Iceberg table references (branches and tags).
475
+
476
+ The refs table contains:
477
+ - name: Name of the reference
478
+ - type: Type of reference (BRANCH or TAG)
479
+ - snapshot_id: ID of referenced snapshot
480
+ - max_reference_age_in_ms: Max age before reference expiry
481
+ - min_snapshots_to_keep: Min snapshots to keep (branches only)
482
+ - max_snapshot_age_in_ms: Max snapshot age in branch
483
+
484
+ Args:
485
+ catalog: catalog name
486
+ schema_name: schema name
487
+ table: The name of the table
488
+
489
+ Returns:
490
+ str: JSON-formatted table references
491
+ """
492
+ return client.show_refs(catalog, schema_name, table)
493
+
494
+
495
+ # Prompts
496
+ @mcp.prompt(title="Explore Trino Data")
497
+ def explore_data(catalog: str, schema_name: str) -> list[base.Message]:
498
+ """Interactive prompt to explore Trino data.
499
+
500
+ Args:
501
+ catalog: The name of the catalog to explore
502
+ schema_name: The name of the schema to explore
503
+
504
+ Returns:
505
+ list[base.Message]: A list of messages to guide the conversation
506
+ """
507
+ messages = [
508
+ base.AssistantMessage(
509
+ "I'll help you explore data in Trino. I can show you available catalogs, "
510
+ "schemas, and tables, and help you query the data."
511
+ )
512
+ ]
513
+
514
+ if catalog and schema_name:
515
+ messages.append(
516
+ base.UserMessage(
517
+ f"Show me what tables are available in the {catalog}.{schema_name} schema and help me query them."
518
+ )
519
+ )
520
+ elif catalog:
521
+ messages.append(base.UserMessage(f"Show me what schemas are available in the {catalog} catalog."))
522
+ else:
523
+ messages.append(base.UserMessage("Show me what catalogs are available."))
524
+
525
+ return messages
526
+
527
+
528
+ @mcp.prompt(title="Maintain Iceberg Table")
529
+ def maintain_iceberg(table: str, catalog: str, schema_name: str) -> list[base.Message]:
530
+ """Interactive prompt for Iceberg table maintenance."""
531
+ return [
532
+ base.AssistantMessage(
533
+ "I'll help you maintain an Iceberg table. I can help with optimization, "
534
+ "cleaning up snapshots and orphan files, and viewing table metadata."
535
+ ),
536
+ base.UserMessage(
537
+ f"What maintenance operations should we perform on the Iceberg table "
538
+ f"{catalog + '.' if catalog else ''}{schema_name + '.' if schema_name else ''}{table}?"
539
+ ),
540
+ ]
541
+
542
+
543
+ if __name__ == "__main__":
544
+ import argparse
545
+ import sys
546
+
547
+ from loguru import logger
548
+
549
+ # Configure logging
550
+ logger.remove()
551
+ logger.add(sys.stderr, level="INFO", format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}")
552
+
553
+ parser = argparse.ArgumentParser(
554
+ description="MCP Trino Server - Model Context Protocol connector for Trino",
555
+ formatter_class=argparse.RawDescriptionHelpFormatter,
556
+ epilog="""
557
+ Transport modes:
558
+ stdio Standard I/O (default) - for local MCP clients like VS Code
559
+ streamable-http HTTP with streaming support (recommended for remote/web access)
560
+ sse Server-Sent Events (legacy HTTP transport)
561
+
562
+ Examples:
563
+ # Run with stdio (default, for VS Code integration)
564
+ python server.py
565
+
566
+ # Run with Streamable HTTP on port 8000
567
+ python server.py --transport streamable-http --host 0.0.0.0 --port 8000
568
+
569
+ # Run with SSE transport
570
+ python server.py --transport sse --host 127.0.0.1 --port 8001
571
+
572
+ Environment variables:
573
+ MCP_HOST Default host for HTTP transports (default: 127.0.0.1)
574
+ MCP_PORT Default port for HTTP transports (default: 8000)
575
+ """,
576
+ )
577
+ parser.add_argument(
578
+ "--host",
579
+ default=None,
580
+ help="Host to bind to (default: 127.0.0.1, use 0.0.0.0 for all interfaces)",
581
+ )
582
+ parser.add_argument(
583
+ "--port",
584
+ type=int,
585
+ default=None,
586
+ help="Port to listen on (default: 8000)",
587
+ )
588
+ parser.add_argument(
589
+ "--transport",
590
+ choices=["stdio", "streamable-http", "sse"],
591
+ default="stdio",
592
+ help="Transport type (default: stdio)",
593
+ )
594
+ args = parser.parse_args()
595
+
596
+ # Update settings if CLI args provided (override env vars)
597
+ if args.host:
598
+ mcp.settings.host = args.host
599
+ if args.port:
600
+ mcp.settings.port = args.port
601
+
602
+ logger.info(f"Starting Trino MCP server with {args.transport} transport")
603
+
604
+ if args.transport == "stdio":
605
+ logger.info("Using stdio transport for local MCP communication")
606
+ mcp.run(transport="stdio")
607
+ elif args.transport == "streamable-http":
608
+ logger.info(f"Starting Streamable HTTP server on http://{mcp.settings.host}:{mcp.settings.port}/mcp")
609
+ mcp.run(transport="streamable-http")
610
+ elif args.transport == "sse":
611
+ logger.info(f"Starting SSE server on http://{mcp.settings.host}:{mcp.settings.port}/sse")
612
+ mcp.run(transport="sse")
613
+
614
+
615
+ def main():
616
+ """Entry point for the MCP Trino server."""
617
+ mcp.run(transport="stdio")