1
+ import importlib .metadata
2
+ import json
1
3
import logging
2
- from functools import wraps
3
- from typing import Any , Callable
4
4
import os
5
- import uuid
6
- import yaml
7
- import importlib .metadata
8
5
import time
6
+ import uuid
7
+ from functools import wraps
8
+ from typing import Any , Callable
9
9
10
- from mcp .server .models import InitializationOptions
10
+ import mcp .server .stdio
11
11
import mcp .types as types
12
+ import yaml
12
13
from mcp .server import NotificationOptions , Server
13
- import mcp .server .stdio
14
+ from mcp .server .models import InitializationOptions
14
15
from pydantic import AnyUrl , BaseModel
15
16
from snowflake .snowpark import Session
16
17
@@ -113,8 +114,22 @@ async def handle_list_tables(arguments, db, *_):
113
114
FROM { db .connection_config ['database' ]} .information_schema.tables
114
115
WHERE table_schema = '{ db .connection_config ['schema' ].upper ()} '
115
116
"""
116
- results , data_id = db .execute_query (query )
117
- return [types .TextContent (type = "text" , text = data_to_yaml (results ), artifacts = [{"type" : "dataframe" , "data" : results }])]
117
+ data , data_id = db .execute_query (query )
118
+
119
+ output = {
120
+ "type" : "data" ,
121
+ "data_id" : data_id ,
122
+ "data" : data ,
123
+ }
124
+ yaml_output = data_to_yaml (output )
125
+ json_output = json .dumps (output )
126
+ return [
127
+ types .TextContent (type = "text" , text = yaml_output ),
128
+ types .EmbeddedResource (
129
+ type = "resource" ,
130
+ resource = types .TextResourceContents (uri = f"data://{ data_id } " , text = json_output , mimeType = "application/json" ),
131
+ ),
132
+ ]
118
133
119
134
120
135
async def handle_describe_table (arguments , db , * _ ):
@@ -131,30 +146,41 @@ async def handle_describe_table(arguments, db, *_):
131
146
FROM { database_name } .information_schema.columns
132
147
WHERE table_schema = '{ schema_name } ' AND table_name = '{ table_name } '
133
148
"""
134
- results , data_id = db .execute_query (query )
149
+ data , data_id = db .execute_query (query )
150
+
151
+ output = {
152
+ "type" : "data" ,
153
+ "data_id" : data_id ,
154
+ "data" : data ,
155
+ }
156
+ yaml_output = data_to_yaml (output )
157
+ json_output = json .dumps (output )
135
158
return [
136
- types .TextContent (
137
- type = "text" , text = data_to_yaml (results ), artifacts = [{"type" : "dataframe" , "data" : results , "data_id" : data_id }]
138
- )
159
+ types .TextContent (type = "text" , text = yaml_output ),
160
+ types .EmbeddedResource (
161
+ type = "resource" ,
162
+ resource = types .TextResourceContents (uri = f"data://{ data_id } " , text = json_output , mimeType = "application/json" ),
163
+ ),
139
164
]
140
165
141
166
142
167
async def handle_read_query (arguments , db , write_detector , * _ ):
143
- MAX_RESULTS = 50
144
168
if write_detector .analyze_query (arguments ["query" ])["contains_write" ]:
145
169
raise ValueError ("Calls to read_query should not contain write operations" )
146
-
147
- results , data_id = db . execute_query ( arguments [ "query" ])
148
- truncate = len ( results ) > MAX_RESULTS
149
- results_text = data_to_yaml ( results [: MAX_RESULTS ])
150
- if truncate :
151
- results_text += f" \n Results of query have been truncated. There are { len ( results ) - MAX_RESULTS } more rows."
152
- results_text += f" \n data_id = { data_id } "
153
-
170
+ data , data_id = db . execute_query ( arguments [ "query" ])
171
+ output = {
172
+ "type" : "data" ,
173
+ "data_id" : data_id ,
174
+ "data" : data ,
175
+ }
176
+ yaml_output = data_to_yaml ( output )
177
+ json_output = json . dumps ( output )
154
178
return [
155
- types .TextContent (
156
- type = "text" , text = results_text , artifacts = [{"type" : "dataframe" , "data" : results , "data_id" : data_id }]
157
- )
179
+ types .TextContent (type = "text" , text = yaml_output ),
180
+ types .EmbeddedResource (
181
+ type = "resource" ,
182
+ resource = types .TextResourceContents (uri = f"data://{ data_id } " , text = json_output , mimeType = "application/json" ),
183
+ ),
158
184
]
159
185
160
186
@@ -208,9 +234,11 @@ async def prefetch_tables(db: SnowflakeDB, credentials: dict) -> str:
208
234
tables_brief [row ["TABLE_NAME" ]] = {** row , "COLUMNS" : {}}
209
235
210
236
for row in column_results :
211
- tables_brief [row ["TABLE_NAME" ]]["COLUMNS" ][row ["COLUMN_NAME" ]] = row
237
+ row_without_table_name = row .copy ()
238
+ del row_without_table_name ["TABLE_NAME" ]
239
+ tables_brief [row ["TABLE_NAME" ]]["COLUMNS" ][row ["COLUMN_NAME" ]] = row_without_table_name
212
240
213
- return data_to_yaml ( tables_brief )
241
+ return tables_brief
214
242
215
243
except Exception as e :
216
244
logger .error (f"Error prefetching table descriptions: { e } " )
@@ -238,7 +266,8 @@ async def main(
238
266
server = Server ("snowflake-manager" )
239
267
write_detector = SQLWriteDetector ()
240
268
241
- tables_brief = await prefetch_tables (db , credentials ) if prefetch else ""
269
+ tables_info = await prefetch_tables (db , credentials )
270
+ tables_brief = data_to_yaml (tables_info ) if prefetch else ""
242
271
243
272
all_tools = [
244
273
Tool (
@@ -319,27 +348,36 @@ async def main(
319
348
# Register handlers
320
349
@server .list_resources ()
321
350
async def handle_list_resources () -> list [types .Resource ]:
322
- return [
351
+ resources = [
323
352
types .Resource (
324
353
uri = AnyUrl ("memo://insights" ),
325
354
name = "Data Insights Memo" ,
326
355
description = "A living document of discovered data insights" ,
327
356
mimeType = "text/plain" ,
328
- ),
357
+ )
358
+ ]
359
+ table_brief_resources = [
329
360
types .Resource (
330
- uri = AnyUrl ("context://tables " ),
331
- name = "Tables " ,
332
- description = "Description of tables and columns in the database " ,
361
+ uri = AnyUrl (f "context://table/ { table_name } " ),
362
+ name = f" { table_name } table " ,
363
+ description = f "Description of the { table_name } table " ,
333
364
mimeType = "text/plain" ,
334
- ),
365
+ )
366
+ for table_name in tables_info .keys ()
335
367
]
368
+ resources += table_brief_resources
369
+ return resources
336
370
337
371
@server .read_resource ()
338
372
async def handle_read_resource (uri : AnyUrl ) -> str :
339
373
if str (uri ) == "memo://insights" :
340
374
return db .get_memo ()
341
- elif str (uri ) == "context://tables" :
342
- return tables_brief
375
+ elif str (uri ).startswith ("context://table" ):
376
+ table_name = str (uri ).split ("/" )[- 1 ]
377
+ if table_name in tables_info :
378
+ return data_to_yaml (tables_info [table_name ])
379
+ else :
380
+ raise ValueError (f"Unknown table: { table_name } " )
343
381
else :
344
382
raise ValueError (f"Unknown resource: { uri } " )
345
383
0 commit comments