awslabs.documentdb-mcp-server 0.0.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.
- awslabs/documentdb_mcp_server/__init__.py +14 -0
- awslabs/documentdb_mcp_server/analytic_tools.py +375 -0
- awslabs/documentdb_mcp_server/config.py +30 -0
- awslabs/documentdb_mcp_server/connection_tools.py +223 -0
- awslabs/documentdb_mcp_server/db_management_tools.py +176 -0
- awslabs/documentdb_mcp_server/query_tools.py +121 -0
- awslabs/documentdb_mcp_server/server.py +159 -0
- awslabs/documentdb_mcp_server/write_tools.py +202 -0
- awslabs_documentdb_mcp_server-0.0.1.dist-info/METADATA +202 -0
- awslabs_documentdb_mcp_server-0.0.1.dist-info/RECORD +14 -0
- awslabs_documentdb_mcp_server-0.0.1.dist-info/WHEEL +4 -0
- awslabs_documentdb_mcp_server-0.0.1.dist-info/entry_points.txt +2 -0
- awslabs_documentdb_mcp_server-0.0.1.dist-info/licenses/LICENSE +175 -0
- awslabs_documentdb_mcp_server-0.0.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
|
|
4
|
+
# with the License. A copy of the License is located at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
|
|
9
|
+
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
|
+
# and limitations under the License.
|
|
11
|
+
|
|
12
|
+
"""Database management tools for DocumentDB MCP Server."""
|
|
13
|
+
|
|
14
|
+
from awslabs.documentdb_mcp_server.config import serverConfig
|
|
15
|
+
from awslabs.documentdb_mcp_server.connection_tools import DocumentDBConnection
|
|
16
|
+
from loguru import logger
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
from typing import Annotated, Any, Dict, List
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def list_databases(
|
|
22
|
+
connection_id: Annotated[
|
|
23
|
+
str, Field(description='The connection ID returned by the connect tool')
|
|
24
|
+
],
|
|
25
|
+
) -> Dict[str, Any]:
|
|
26
|
+
"""List all available databases in the DocumentDB cluster.
|
|
27
|
+
|
|
28
|
+
This tool returns the names of all accessible databases in the connected cluster.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dict[str, Any]: List of database names
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
client = DocumentDBConnection.get_connection(connection_id)
|
|
35
|
+
databases = client.list_database_names()
|
|
36
|
+
logger.info(f'Found {len(databases)} databases')
|
|
37
|
+
return {'databases': databases, 'count': len(databases)}
|
|
38
|
+
except ValueError as e:
|
|
39
|
+
logger.error(f'Connection error: {str(e)}')
|
|
40
|
+
raise ValueError(str(e))
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f'Error listing databases: {str(e)}')
|
|
43
|
+
raise ValueError(f'Failed to list databases: {str(e)}')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def create_collection(
|
|
47
|
+
connection_id: Annotated[
|
|
48
|
+
str, Field(description='The connection ID returned by the connect tool')
|
|
49
|
+
],
|
|
50
|
+
database: Annotated[str, Field(description='Name of the database')],
|
|
51
|
+
collection: Annotated[str, Field(description='Name of the collection to create')],
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
"""Create a new collection in a DocumentDB database.
|
|
54
|
+
|
|
55
|
+
This tool creates a new collection in the specified database.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict[str, Any]: Status of collection creation
|
|
59
|
+
"""
|
|
60
|
+
# Check if server is in read-only mode
|
|
61
|
+
if serverConfig.read_only_mode:
|
|
62
|
+
logger.warning('Create collection operation denied: Server is in read-only mode')
|
|
63
|
+
raise ValueError('Operation not permitted: Server is configured in read-only mode')
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Get connection
|
|
67
|
+
if connection_id not in DocumentDBConnection._connections:
|
|
68
|
+
raise ValueError(f'Connection ID {connection_id} not found. You must connect first.')
|
|
69
|
+
|
|
70
|
+
connection_info = DocumentDBConnection._connections[connection_id]
|
|
71
|
+
client = connection_info.client
|
|
72
|
+
|
|
73
|
+
db = client[database]
|
|
74
|
+
|
|
75
|
+
# Check if collection already exists
|
|
76
|
+
existing_collections = db.list_collection_names()
|
|
77
|
+
if collection in existing_collections:
|
|
78
|
+
return {
|
|
79
|
+
'success': False,
|
|
80
|
+
'message': f"Collection '{collection}' already exists in database '{database}'",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Create the collection
|
|
84
|
+
db.create_collection(collection)
|
|
85
|
+
|
|
86
|
+
logger.info(f"Created collection '{collection}' in database '{database}'")
|
|
87
|
+
return {'success': True, 'message': f"Collection '{collection}' created successfully"}
|
|
88
|
+
except ValueError as e:
|
|
89
|
+
logger.error(f'Connection error: {str(e)}')
|
|
90
|
+
raise ValueError(str(e))
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f'Error creating collection: {str(e)}')
|
|
93
|
+
raise ValueError(f'Failed to create collection: {str(e)}')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def list_collections(
|
|
97
|
+
connection_id: Annotated[
|
|
98
|
+
str, Field(description='The connection ID returned by the connect tool')
|
|
99
|
+
],
|
|
100
|
+
database: Annotated[str, Field(description='Name of the database')],
|
|
101
|
+
) -> List[str]:
|
|
102
|
+
"""List collections in a DocumentDB database.
|
|
103
|
+
|
|
104
|
+
This tool returns the names of all collections in a specified database.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List[str]: List of collection names
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
# Get connection
|
|
111
|
+
if connection_id not in DocumentDBConnection._connections:
|
|
112
|
+
raise ValueError(f'Connection ID {connection_id} not found. You must connect first.')
|
|
113
|
+
|
|
114
|
+
connection_info = DocumentDBConnection._connections[connection_id]
|
|
115
|
+
client = connection_info.client
|
|
116
|
+
db = client[database]
|
|
117
|
+
collections = db.list_collection_names()
|
|
118
|
+
logger.info(f"Found {len(collections)} collections in database '{database}'")
|
|
119
|
+
return collections
|
|
120
|
+
except ValueError as e:
|
|
121
|
+
logger.error(f'Connection error: {str(e)}')
|
|
122
|
+
raise ValueError(str(e))
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f'Error listing collections: {str(e)}')
|
|
125
|
+
raise ValueError(f'Failed to list collections: {str(e)}')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def drop_collection(
|
|
129
|
+
connection_id: Annotated[
|
|
130
|
+
str, Field(description='The connection ID returned by the connect tool')
|
|
131
|
+
],
|
|
132
|
+
database: Annotated[str, Field(description='Name of the database')],
|
|
133
|
+
collection: Annotated[str, Field(description='Name of the collection to drop')],
|
|
134
|
+
) -> Dict[str, Any]:
|
|
135
|
+
"""Drop a collection from a DocumentDB database.
|
|
136
|
+
|
|
137
|
+
This tool completely removes a collection and all its documents from the specified database.
|
|
138
|
+
This operation cannot be undone, so use it with caution.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Dict[str, Any]: Status of the drop operation
|
|
142
|
+
"""
|
|
143
|
+
# Check if server is in read-only mode
|
|
144
|
+
if serverConfig.read_only_mode:
|
|
145
|
+
logger.warning('Drop collection operation denied: Server is in read-only mode')
|
|
146
|
+
raise ValueError('Operation not permitted: Server is configured in read-only mode')
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
# Get connection
|
|
150
|
+
if connection_id not in DocumentDBConnection._connections:
|
|
151
|
+
raise ValueError(f'Connection ID {connection_id} not found. You must connect first.')
|
|
152
|
+
|
|
153
|
+
connection_info = DocumentDBConnection._connections[connection_id]
|
|
154
|
+
client = connection_info.client
|
|
155
|
+
|
|
156
|
+
db = client[database]
|
|
157
|
+
|
|
158
|
+
# Check if collection exists
|
|
159
|
+
existing_collections = db.list_collection_names()
|
|
160
|
+
if collection not in existing_collections:
|
|
161
|
+
return {
|
|
162
|
+
'success': False,
|
|
163
|
+
'message': f"Collection '{collection}' does not exist in database '{database}'",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Drop the collection
|
|
167
|
+
db.drop_collection(collection)
|
|
168
|
+
|
|
169
|
+
logger.info(f"Dropped collection '{collection}' from database '{database}'")
|
|
170
|
+
return {'success': True, 'message': f"Collection '{collection}' dropped successfully"}
|
|
171
|
+
except ValueError as e:
|
|
172
|
+
logger.error(f'Connection error: {str(e)}')
|
|
173
|
+
raise ValueError(str(e))
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f'Error dropping collection: {str(e)}')
|
|
176
|
+
raise ValueError(f'Failed to drop collection: {str(e)}')
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
|
|
4
|
+
# with the License. A copy of the License is located at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
|
|
9
|
+
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
|
+
# and limitations under the License.
|
|
11
|
+
|
|
12
|
+
"""Query tools for DocumentDB MCP Server."""
|
|
13
|
+
|
|
14
|
+
from awslabs.documentdb_mcp_server.connection_tools import DocumentDBConnection
|
|
15
|
+
from loguru import logger
|
|
16
|
+
from pydantic import Field
|
|
17
|
+
from typing import Annotated, Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def find(
|
|
21
|
+
connection_id: Annotated[
|
|
22
|
+
str, Field(description='The connection ID returned by the connect tool')
|
|
23
|
+
],
|
|
24
|
+
database: Annotated[str, Field(description='Name of the database')],
|
|
25
|
+
collection: Annotated[str, Field(description='Name of the collection')],
|
|
26
|
+
query: Annotated[
|
|
27
|
+
Dict[str, Any], Field(description='Query filter (e.g., {"name": "example"})')
|
|
28
|
+
],
|
|
29
|
+
projection: Annotated[
|
|
30
|
+
Optional[Dict[str, Any]],
|
|
31
|
+
Field(description='Fields to include/exclude (e.g., {"_id": 0, "name": 1})'),
|
|
32
|
+
] = None,
|
|
33
|
+
limit: Annotated[
|
|
34
|
+
int, Field(description='Maximum number of documents to return (default: 10)')
|
|
35
|
+
] = 10,
|
|
36
|
+
) -> List[Dict[str, Any]]:
|
|
37
|
+
"""Run a query against a DocumentDB collection.
|
|
38
|
+
|
|
39
|
+
This tool queries documents from a specified collection based on a filter.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List[Dict[str, Any]]: List of matching documents
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
# Get connection
|
|
46
|
+
if connection_id not in DocumentDBConnection._connections:
|
|
47
|
+
raise ValueError(f'Connection ID {connection_id} not found. You must connect first.')
|
|
48
|
+
|
|
49
|
+
connection_info = DocumentDBConnection._connections[connection_id]
|
|
50
|
+
client = connection_info.client
|
|
51
|
+
|
|
52
|
+
db = client[database]
|
|
53
|
+
coll = db[collection]
|
|
54
|
+
|
|
55
|
+
result = list(coll.find(query, projection).limit(limit))
|
|
56
|
+
|
|
57
|
+
# Convert ObjectId to string for JSON serialization
|
|
58
|
+
for doc in result:
|
|
59
|
+
if '_id' in doc and not isinstance(doc['_id'], str):
|
|
60
|
+
doc['_id'] = str(doc['_id'])
|
|
61
|
+
|
|
62
|
+
logger.info(f'Query returned {len(result)} documents')
|
|
63
|
+
return result
|
|
64
|
+
except ValueError as e:
|
|
65
|
+
logger.error(f'Connection error: {str(e)}')
|
|
66
|
+
raise ValueError(str(e))
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f'Error querying DocumentDB: {str(e)}')
|
|
69
|
+
raise ValueError(f'Failed to query DocumentDB: {str(e)}')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def aggregate(
|
|
73
|
+
connection_id: Annotated[
|
|
74
|
+
str, Field(description='The connection ID returned by the connect tool')
|
|
75
|
+
],
|
|
76
|
+
database: Annotated[str, Field(description='Name of the database')],
|
|
77
|
+
collection: Annotated[str, Field(description='Name of the collection')],
|
|
78
|
+
pipeline: Annotated[
|
|
79
|
+
List[Dict[str, Any]], Field(description='DocumentDB aggregation pipeline')
|
|
80
|
+
],
|
|
81
|
+
limit: Annotated[
|
|
82
|
+
int, Field(description='Maximum number of documents to return (default: 10)')
|
|
83
|
+
] = 10,
|
|
84
|
+
) -> List[Dict[str, Any]]:
|
|
85
|
+
"""Run an aggregation pipeline against a DocumentDB collection.
|
|
86
|
+
|
|
87
|
+
This tool executes a DocumentDB aggregation pipeline on a specified collection.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List[Dict[str, Any]]: List of aggregation results
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
# Get connection
|
|
94
|
+
if connection_id not in DocumentDBConnection._connections:
|
|
95
|
+
raise ValueError(f'Connection ID {connection_id} not found. You must connect first.')
|
|
96
|
+
|
|
97
|
+
connection_info = DocumentDBConnection._connections[connection_id]
|
|
98
|
+
client = connection_info.client
|
|
99
|
+
|
|
100
|
+
db = client[database]
|
|
101
|
+
coll = db[collection]
|
|
102
|
+
|
|
103
|
+
# Add limit stage if not already in pipeline
|
|
104
|
+
if limit > 0 and not any('$limit' in stage for stage in pipeline):
|
|
105
|
+
pipeline.append({'$limit': limit})
|
|
106
|
+
|
|
107
|
+
result = list(coll.aggregate(pipeline))
|
|
108
|
+
|
|
109
|
+
# Convert ObjectId to string for JSON serialization
|
|
110
|
+
for doc in result:
|
|
111
|
+
if '_id' in doc and not isinstance(doc['_id'], str):
|
|
112
|
+
doc['_id'] = str(doc['_id'])
|
|
113
|
+
|
|
114
|
+
logger.info(f'Aggregation returned {len(result)} results')
|
|
115
|
+
return result
|
|
116
|
+
except ValueError as e:
|
|
117
|
+
logger.error(f'Connection error: {str(e)}')
|
|
118
|
+
raise ValueError(str(e))
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f'Error running aggregation in DocumentDB: {str(e)}')
|
|
121
|
+
raise ValueError(f'Failed to run aggregation: {str(e)}')
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
|
|
4
|
+
# with the License. A copy of the License is located at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
|
|
9
|
+
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
|
+
# and limitations under the License.
|
|
11
|
+
|
|
12
|
+
"""AWS Labs DocumentDB MCP Server implementation for querying AWS DocumentDB."""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
from awslabs.documentdb_mcp_server.analytic_tools import (
|
|
16
|
+
analyze_schema,
|
|
17
|
+
count_documents,
|
|
18
|
+
explain_operation,
|
|
19
|
+
get_collection_stats,
|
|
20
|
+
get_database_stats,
|
|
21
|
+
)
|
|
22
|
+
from awslabs.documentdb_mcp_server.config import serverConfig
|
|
23
|
+
from awslabs.documentdb_mcp_server.connection_tools import (
|
|
24
|
+
DocumentDBConnection,
|
|
25
|
+
connect,
|
|
26
|
+
disconnect,
|
|
27
|
+
)
|
|
28
|
+
from awslabs.documentdb_mcp_server.db_management_tools import (
|
|
29
|
+
create_collection,
|
|
30
|
+
drop_collection,
|
|
31
|
+
list_collections,
|
|
32
|
+
list_databases,
|
|
33
|
+
)
|
|
34
|
+
from awslabs.documentdb_mcp_server.query_tools import aggregate, find
|
|
35
|
+
from awslabs.documentdb_mcp_server.write_tools import delete, insert, update
|
|
36
|
+
from loguru import logger
|
|
37
|
+
from mcp.server.fastmcp import FastMCP
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Create the FastMCP server
|
|
41
|
+
mcp = FastMCP(
|
|
42
|
+
'awslabs.documentdb-mcp-server',
|
|
43
|
+
instructions="""DocumentDB MCP Server provides tools to connect to and query AWS DocumentDB databases.
|
|
44
|
+
|
|
45
|
+
Usage pattern:
|
|
46
|
+
1. First use the `connect` tool to establish a connection and get a connection_id
|
|
47
|
+
2. Use the connection_id with other tools to perform operations
|
|
48
|
+
3. When finished, use the `disconnect` tool to release resources
|
|
49
|
+
|
|
50
|
+
Server Configuration:
|
|
51
|
+
- The server can be configured in read-only mode, which blocks write operations
|
|
52
|
+
while still allowing read operations.""",
|
|
53
|
+
dependencies=[
|
|
54
|
+
'pydantic',
|
|
55
|
+
'loguru',
|
|
56
|
+
'pymongo',
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Register all tools
|
|
62
|
+
|
|
63
|
+
# Connection tools
|
|
64
|
+
mcp.tool(name='connect')(connect)
|
|
65
|
+
mcp.tool(name='disconnect')(disconnect)
|
|
66
|
+
|
|
67
|
+
# Query tools
|
|
68
|
+
mcp.tool(name='find')(find)
|
|
69
|
+
mcp.tool(name='aggregate')(aggregate)
|
|
70
|
+
|
|
71
|
+
# Write tools
|
|
72
|
+
mcp.tool(name='insert')(insert)
|
|
73
|
+
mcp.tool(name='update')(update)
|
|
74
|
+
mcp.tool(name='delete')(delete)
|
|
75
|
+
|
|
76
|
+
# Database management tools
|
|
77
|
+
mcp.tool(name='listDatabases')(list_databases)
|
|
78
|
+
mcp.tool(name='createCollection')(create_collection)
|
|
79
|
+
mcp.tool(name='listCollections')(list_collections)
|
|
80
|
+
mcp.tool(name='dropCollection')(drop_collection)
|
|
81
|
+
|
|
82
|
+
# Analytic tools
|
|
83
|
+
mcp.tool(name='countDocuments')(count_documents)
|
|
84
|
+
mcp.tool(name='getDatabaseStats')(get_database_stats)
|
|
85
|
+
mcp.tool(name='getCollectionStats')(get_collection_stats)
|
|
86
|
+
mcp.tool(name='analyzeSchema')(analyze_schema)
|
|
87
|
+
mcp.tool(name='explainOperation')(explain_operation)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main():
|
|
91
|
+
"""Run the MCP server with CLI argument support."""
|
|
92
|
+
parser = argparse.ArgumentParser(
|
|
93
|
+
description='An AWS Labs Model Context Protocol (MCP) server for DocumentDB'
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument('--sse', action='store_true', help='Use SSE transport')
|
|
96
|
+
parser.add_argument('--port', type=int, default=8888, help='Port to run the server on')
|
|
97
|
+
parser.add_argument('--host', type=str, default='127.0.0.1', help='Host to bind the server to')
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
'--log-level',
|
|
100
|
+
type=str,
|
|
101
|
+
default='INFO',
|
|
102
|
+
choices=['TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'],
|
|
103
|
+
help='Set the logging level',
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
'--connection-timeout',
|
|
107
|
+
type=int,
|
|
108
|
+
default=30,
|
|
109
|
+
help='Idle connection timeout in minutes (default: 30)',
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
'--allow-write',
|
|
113
|
+
action='store_true',
|
|
114
|
+
help='Allow write operations (insert, update, delete). By default, the server runs in read-only mode.',
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
args = parser.parse_args()
|
|
118
|
+
|
|
119
|
+
# Configure logging
|
|
120
|
+
logger.remove()
|
|
121
|
+
logger.add(
|
|
122
|
+
lambda msg: print(msg),
|
|
123
|
+
level=args.log_level,
|
|
124
|
+
format='<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>',
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
logger.info(f'Starting DocumentDB MCP Server on {args.host}:{args.port}')
|
|
128
|
+
logger.info(f'Log level: {args.log_level}')
|
|
129
|
+
|
|
130
|
+
# Set connection timeout
|
|
131
|
+
DocumentDBConnection._idle_timeout = args.connection_timeout
|
|
132
|
+
logger.info(f'Idle connection timeout: {args.connection_timeout} minutes')
|
|
133
|
+
|
|
134
|
+
# Configure read-only mode
|
|
135
|
+
serverConfig.read_only_mode = not args.allow_write
|
|
136
|
+
if serverConfig.read_only_mode:
|
|
137
|
+
logger.warning('Server is running in READ-ONLY mode. Write operations will be blocked.')
|
|
138
|
+
else:
|
|
139
|
+
logger.info('Server is running with WRITE operations ENABLED. Database can be modified.')
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
# Run server with appropriate transport
|
|
143
|
+
if args.sse:
|
|
144
|
+
mcp.settings.port = args.port
|
|
145
|
+
mcp.settings.host = args.host
|
|
146
|
+
mcp.run(transport='sse')
|
|
147
|
+
else:
|
|
148
|
+
mcp.settings.port = args.port
|
|
149
|
+
mcp.settings.host = args.host
|
|
150
|
+
mcp.run()
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.critical(f'Failed to start server: {str(e)}')
|
|
153
|
+
finally:
|
|
154
|
+
# Close all DB connections
|
|
155
|
+
DocumentDBConnection.close_all_connections()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == '__main__':
|
|
159
|
+
main()
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
|
|
4
|
+
# with the License. A copy of the License is located at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
|
|
9
|
+
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
|
+
# and limitations under the License.
|
|
11
|
+
|
|
12
|
+
"""Write tools for DocumentDB MCP Server."""
|
|
13
|
+
|
|
14
|
+
from awslabs.documentdb_mcp_server.config import serverConfig
|
|
15
|
+
from awslabs.documentdb_mcp_server.connection_tools import DocumentDBConnection
|
|
16
|
+
from loguru import logger
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
from typing import Annotated, Any, Dict, List, Union
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def insert(
|
|
22
|
+
connection_id: Annotated[
|
|
23
|
+
str, Field(description='The connection ID returned by the connect tool')
|
|
24
|
+
],
|
|
25
|
+
database: Annotated[str, Field(description='Name of the database')],
|
|
26
|
+
collection: Annotated[str, Field(description='Name of the collection')],
|
|
27
|
+
documents: Annotated[
|
|
28
|
+
Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
29
|
+
Field(description='Document or list of documents to insert'),
|
|
30
|
+
],
|
|
31
|
+
) -> Dict[str, Any]:
|
|
32
|
+
"""Insert one or more documents into a DocumentDB collection.
|
|
33
|
+
|
|
34
|
+
This tool inserts new documents into a specified collection.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dict[str, Any]: Insert operation results including document IDs
|
|
38
|
+
"""
|
|
39
|
+
# Check if server is in read-only mode
|
|
40
|
+
if serverConfig.read_only_mode:
|
|
41
|
+
logger.warning('Insert operation denied: Server is in read-only mode')
|
|
42
|
+
raise ValueError('Operation not permitted: Server is configured in read-only mode')
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Get connection
|
|
46
|
+
if connection_id not in DocumentDBConnection._connections:
|
|
47
|
+
raise ValueError(f'Connection ID {connection_id} not found. You must connect first.')
|
|
48
|
+
|
|
49
|
+
connection_info = DocumentDBConnection._connections[connection_id]
|
|
50
|
+
client = connection_info.client
|
|
51
|
+
|
|
52
|
+
db = client[database]
|
|
53
|
+
coll = db[collection]
|
|
54
|
+
|
|
55
|
+
# Handle single document or multiple documents
|
|
56
|
+
if isinstance(documents, dict):
|
|
57
|
+
result = coll.insert_one(documents)
|
|
58
|
+
inserted_ids = [str(result.inserted_id)]
|
|
59
|
+
count = 1
|
|
60
|
+
else:
|
|
61
|
+
result = coll.insert_many(documents)
|
|
62
|
+
inserted_ids = [str(id) for id in result.inserted_ids]
|
|
63
|
+
count = len(inserted_ids)
|
|
64
|
+
|
|
65
|
+
logger.info(f'Inserted {count} documents')
|
|
66
|
+
return {'success': True, 'inserted_count': count, 'inserted_ids': inserted_ids}
|
|
67
|
+
except ValueError as e:
|
|
68
|
+
logger.error(f'Connection error: {str(e)}')
|
|
69
|
+
raise ValueError(str(e))
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f'Error inserting into DocumentDB: {str(e)}')
|
|
72
|
+
raise ValueError(f'Failed to insert documents: {str(e)}')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def update(
|
|
76
|
+
connection_id: Annotated[
|
|
77
|
+
str, Field(description='The connection ID returned by the connect tool')
|
|
78
|
+
],
|
|
79
|
+
database: Annotated[str, Field(description='Name of the database')],
|
|
80
|
+
collection: Annotated[str, Field(description='Name of the collection')],
|
|
81
|
+
filter: Annotated[Dict[str, Any], Field(description='Filter to select documents to update')],
|
|
82
|
+
update: Annotated[
|
|
83
|
+
Dict[str, Any],
|
|
84
|
+
Field(
|
|
85
|
+
description='Update operations to apply. It should either include DocumentDB operators like $set, or an entire document if the entire document needs to be replaced.'
|
|
86
|
+
),
|
|
87
|
+
],
|
|
88
|
+
upsert: Annotated[
|
|
89
|
+
bool,
|
|
90
|
+
Field(
|
|
91
|
+
description='Whether to create a new document if no match is found (default: False)'
|
|
92
|
+
),
|
|
93
|
+
] = False,
|
|
94
|
+
many: Annotated[
|
|
95
|
+
bool, Field(description='Whether to update multiple documents (default: False)')
|
|
96
|
+
] = False,
|
|
97
|
+
) -> Dict[str, Any]:
|
|
98
|
+
"""Update documents in a DocumentDB collection.
|
|
99
|
+
|
|
100
|
+
This tool updates existing documents that match a specified filter.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dict[str, Any]: Update operation results
|
|
104
|
+
"""
|
|
105
|
+
# Check if server is in read-only mode
|
|
106
|
+
if serverConfig.read_only_mode:
|
|
107
|
+
logger.warning('Update operation denied: Server is in read-only mode')
|
|
108
|
+
raise ValueError('Operation not permitted: Server is configured in read-only mode')
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Get connection
|
|
112
|
+
if connection_id not in DocumentDBConnection._connections:
|
|
113
|
+
raise ValueError(f'Connection ID {connection_id} not found. You must connect first.')
|
|
114
|
+
|
|
115
|
+
connection_info = DocumentDBConnection._connections[connection_id]
|
|
116
|
+
client = connection_info.client
|
|
117
|
+
|
|
118
|
+
db = client[database]
|
|
119
|
+
coll = db[collection]
|
|
120
|
+
|
|
121
|
+
# If the update doesn't have any operators, then it's a replace
|
|
122
|
+
if not any(key.startswith('$') for key in update.keys()):
|
|
123
|
+
result = coll.replace_one(filter, update, upsert=upsert)
|
|
124
|
+
matched = result.matched_count
|
|
125
|
+
modified = result.modified_count
|
|
126
|
+
# If the update needs to update multiple documents
|
|
127
|
+
elif many:
|
|
128
|
+
result = coll.update_many(filter, update, upsert=upsert)
|
|
129
|
+
matched = result.matched_count
|
|
130
|
+
modified = result.modified_count
|
|
131
|
+
# Else only a single document needs to be updated
|
|
132
|
+
else:
|
|
133
|
+
result = coll.update_one(filter, update, upsert=upsert)
|
|
134
|
+
matched = result.matched_count
|
|
135
|
+
modified = result.modified_count
|
|
136
|
+
|
|
137
|
+
upserted_id = str(result.upserted_id) if result.upserted_id else None
|
|
138
|
+
|
|
139
|
+
logger.info(f'Updated {modified} documents (matched {matched})')
|
|
140
|
+
return {
|
|
141
|
+
'success': True,
|
|
142
|
+
'matched_count': matched,
|
|
143
|
+
'modified_count': modified,
|
|
144
|
+
'upserted_id': upserted_id,
|
|
145
|
+
}
|
|
146
|
+
except ValueError as e:
|
|
147
|
+
logger.error(f'Connection error: {str(e)}')
|
|
148
|
+
raise ValueError(str(e))
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f'Error updating DocumentDB: {str(e)}')
|
|
151
|
+
raise ValueError(f'Failed to update documents: {str(e)}')
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def delete(
|
|
155
|
+
connection_id: Annotated[
|
|
156
|
+
str, Field(description='The connection ID returned by the connect tool')
|
|
157
|
+
],
|
|
158
|
+
database: Annotated[str, Field(description='Name of the database')],
|
|
159
|
+
collection: Annotated[str, Field(description='Name of the collection')],
|
|
160
|
+
filter: Annotated[Dict[str, Any], Field(description='Filter to select documents to delete')],
|
|
161
|
+
many: Annotated[
|
|
162
|
+
bool, Field(description='Whether to delete multiple documents (default: False)')
|
|
163
|
+
] = False,
|
|
164
|
+
) -> Dict[str, Any]:
|
|
165
|
+
"""Delete documents from a DocumentDB collection.
|
|
166
|
+
|
|
167
|
+
This tool deletes documents that match a specified filter.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Dict[str, Any]: Delete operation results
|
|
171
|
+
"""
|
|
172
|
+
# Check if server is in read-only mode
|
|
173
|
+
if serverConfig.read_only_mode:
|
|
174
|
+
logger.warning('Delete operation denied: Server is in read-only mode')
|
|
175
|
+
raise ValueError('Operation not permitted: Server is configured in read-only mode')
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# Get connection
|
|
179
|
+
if connection_id not in DocumentDBConnection._connections:
|
|
180
|
+
raise ValueError(f'Connection ID {connection_id} not found. You must connect first.')
|
|
181
|
+
|
|
182
|
+
connection_info = DocumentDBConnection._connections[connection_id]
|
|
183
|
+
client = connection_info.client
|
|
184
|
+
|
|
185
|
+
db = client[database]
|
|
186
|
+
coll = db[collection]
|
|
187
|
+
|
|
188
|
+
if many:
|
|
189
|
+
result = coll.delete_many(filter)
|
|
190
|
+
deleted = result.deleted_count
|
|
191
|
+
else:
|
|
192
|
+
result = coll.delete_one(filter)
|
|
193
|
+
deleted = result.deleted_count
|
|
194
|
+
|
|
195
|
+
logger.info(f'Deleted {deleted} documents')
|
|
196
|
+
return {'success': True, 'deleted_count': deleted}
|
|
197
|
+
except ValueError as e:
|
|
198
|
+
logger.error(f'Connection error: {str(e)}')
|
|
199
|
+
raise ValueError(str(e))
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.error(f'Error deleting from DocumentDB: {str(e)}')
|
|
202
|
+
raise ValueError(f'Failed to delete documents: {str(e)}')
|