couchbase-mcp-server 0.5.2rc6__py3-none-any.whl → 0.6.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: couchbase-mcp-server
3
- Version: 0.5.2rc6
3
+ Version: 0.6.0
4
4
  Summary: Couchbase MCP Server - The Developer Data Platform for Critical Applications in Our AI World
5
5
  Project-URL: Homepage, https://github.com/Couchbase-Ecosystem/mcp-server-couchbase
6
6
  Project-URL: Documentation, https://github.com/Couchbase-Ecosystem/mcp-server-couchbase#readme
@@ -19,6 +19,9 @@ Requires-Dist: mcp[cli]<2.0.0,>=1.20.0
19
19
  Requires-Dist: urllib3>=2.0.0
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: pre-commit==4.2.0; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio==0.24.0; extra == 'dev'
23
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
24
+ Requires-Dist: pytest==8.3.3; extra == 'dev'
22
25
  Requires-Dist: ruff==0.12.5; extra == 'dev'
23
26
  Description-Content-Type: text/markdown
24
27
 
@@ -34,23 +37,49 @@ An [MCP](https://modelcontextprotocol.io/) server implementation of Couchbase th
34
37
 
35
38
  <!-- mcp-name: io.github.Couchbase-Ecosystem/mcp-server-couchbase -->
36
39
 
37
- ## Features
38
-
39
- - Get a list of all the buckets in the cluster
40
- - Get a list of all the scopes and collections in the specified bucket
41
- - Get a list of all the scopes in the specified bucket
42
- - Get a list of all the collections in a specified scope and bucket. Note that this tool requires the cluster to have Query service.
43
- - Get the structure for a collection
44
- - Get a document by ID from a specified scope and collection
45
- - Upsert a document by ID to a specified scope and collection
46
- - Delete a document by ID from a specified scope and collection
47
- - Run a [SQL++ query](https://www.couchbase.com/sqlplusplus/) on a specified scope
48
- - There is an option in the MCP server, `CB_MCP_READ_ONLY_QUERY_MODE` that is set to true by default to disable running SQL++ queries that change the data or the underlying collection structure. Note that the documents can still be updated by ID.
49
- - Get the status of the MCP server
50
- - Check the cluster credentials by connecting to the cluster
51
- - List all indexes in the cluster with their definitions, with optional filtering by bucket, scope, collection and index name.
52
- - Get index recommendations from Couchbase Index Advisor for a given SQL++ query to optimize query performance
53
- - Get cluster health status and list of all running services
40
+ ## Features/Tools
41
+ ### Cluster setup & health tools
42
+ | Tool Name | Description |
43
+ |-----------|-------------|
44
+ | `get_server_configuration_status` | Get the status of the MCP server |
45
+ | `test_cluster_connection` | Check the cluster credentials by connecting to the cluster |
46
+ | `get_cluster_health_and_services` | Get cluster health status and list of all running services |
47
+
48
+ ### Data model & schema discovery tools
49
+ | Tool Name | Description |
50
+ |-----------|-------------|
51
+ | `get_buckets_in_cluster` | Get a list of all the buckets in the cluster |
52
+ | `get_scopes_in_bucket` | Get a list of all the scopes in the specified bucket |
53
+ | `get_collections_in_scope` | Get a list of all the collections in a specified scope and bucket. Note that this tool requires the cluster to have Query service. |
54
+ | `get_scopes_and_collections_in_bucket` | Get a list of all the scopes and collections in the specified bucket |
55
+ | `get_schema_for_collection` | Get the structure for a collection |
56
+
57
+ ### Document KV operations tools
58
+ | Tool Name | Description |
59
+ |-----------|-------------|
60
+ | `get_document_by_id` | Get a document by ID from a specified scope and collection |
61
+ | `upsert_document_by_id` | Upsert a document by ID to a specified scope and collection. **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |
62
+ | `insert_document_by_id` | Insert a new document by ID (fails if document exists). **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |
63
+ | `replace_document_by_id` | Replace an existing document by ID (fails if document doesn't exist). **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |
64
+ | `delete_document_by_id` | Delete a document by ID from a specified scope and collection. **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |
65
+
66
+ ### Query and indexing tools
67
+ | Tool Name | Description |
68
+ |-----------|-------------|
69
+ | `list_indexes` | List all indexes in the cluster with their definitions, with optional filtering by bucket, scope, collection and index name. |
70
+ | `get_index_advisor_recommendations` | Get index recommendations from Couchbase Index Advisor for a given SQL++ query to optimize query performance |
71
+ | `run_sql_plus_plus_query` | Run a [SQL++ query](https://www.couchbase.com/sqlplusplus/) on a specified scope.<br><br>Queries are automatically scoped to the specified bucket and scope, so use collection names directly (e.g., `SELECT * FROM users` instead of `SELECT * FROM bucket.scope.users`).<br><br>`CB_MCP_READ_ONLY_MODE` is `true` by default, which means that **all write operations (KV and Query)** are disabled. When enabled, KV write tools are not loaded and SQL++ queries that modify data are blocked. |
72
+
73
+ ### Query performance analysis tools
74
+ | Tool Name | Description |
75
+ |-----------|-------------|
76
+ | `get_longest_running_queries` | Get longest running queries by average service time |
77
+ | `get_most_frequent_queries` | Get most frequently executed queries |
78
+ | `get_queries_with_largest_response_sizes` | Get queries with the largest response sizes |
79
+ | `get_queries_with_large_result_count` | Get queries with the largest result counts |
80
+ | `get_queries_using_primary_index` | Get queries that use a primary index (potential performance concern) |
81
+ | `get_queries_not_using_covering_index` | Get queries that don't use a covering index |
82
+ | `get_queries_not_selective` | Get queries that are not selective (index scans return many more documents than final result) |
54
83
 
55
84
  ## Prerequisites
56
85
 
@@ -159,21 +188,135 @@ The server can be configured using environment variables or command line argumen
159
188
  | `CB_CLIENT_CERT_PATH` | `--client-cert-path` | Path to the client certificate file for mTLS authentication| **Required if using mTLS (or Username and Password required)** |
160
189
  | `CB_CLIENT_KEY_PATH` | `--client-key-path` | Path to the client key file for mTLS authentication| **Required if using mTLS (or Username and Password required)** |
161
190
  | `CB_CA_CERT_PATH` | `--ca-cert-path` | Path to server root certificate for TLS if server is configured with a self-signed/untrusted certificate. This will not be required if you are connecting to Capella | |
162
- | `CB_MCP_READ_ONLY_QUERY_MODE` | `--read-only-query-mode` | Prevent data modification queries | `true` |
191
+ | `CB_MCP_READ_ONLY_MODE` | `--read-only-mode` | Prevent all data modifications (KV and Query). When enabled, KV write tools are not loaded. | `true` |
192
+ | `CB_MCP_READ_ONLY_QUERY_MODE` | `--read-only-query-mode` | **[DEPRECATED]** Prevent queries that modify data. Note that data modification would still be possible via document operations tools. Use `CB_MCP_READ_ONLY_MODE` instead. | `true` |
163
193
  | `CB_MCP_TRANSPORT` | `--transport` | Transport mode: `stdio`, `http`, `sse` | `stdio` |
164
194
  | `CB_MCP_HOST` | `--host` | Host for HTTP/SSE transport modes | `127.0.0.1` |
165
195
  | `CB_MCP_PORT` | `--port` | Port for HTTP/SSE transport modes | `8000` |
196
+ | `CB_MCP_DISABLED_TOOLS` | `--disabled-tools` | Tools to disable (see [Disabling Tools](#disabling-tools)) | None |
197
+
198
+ #### Read-Only Mode Configuration
199
+
200
+ The MCP server provides two configuration options for controlling write operations:
201
+
202
+ **`CB_MCP_READ_ONLY_MODE`** (Recommended)
203
+ - When `true` (default): All write operations are disabled. KV write tools (upsert, insert, replace, delete) are **not loaded** and will not be available to the LLM.
204
+ - When `false`: KV write tools are loaded and available.
205
+
206
+ **`CB_MCP_READ_ONLY_QUERY_MODE`** (Deprecated)
207
+ - This option only controls SQL++ query-based writes but does not prevent KV write operations.
208
+ - **Deprecated**: Use `CB_MCP_READ_ONLY_MODE` instead for comprehensive protection.
209
+
210
+ **Mode Behavior Truth Table:**
211
+
212
+ | `READ_ONLY_MODE` | `READ_ONLY_QUERY_MODE` | Result |
213
+ |------------------|------------------------|--------|
214
+ | `true` | `true` | Read-only KV and Query operations. All writes disabled. |
215
+ | `true` | `false` | Read-only KV and Query operations. All writes disabled. |
216
+ | `false` | `true` | Only Query writes disabled. KV writes allowed. |
217
+ | `false` | `false` | All KV and Query operations allowed. |
218
+
219
+ > **Important**: When `READ_ONLY_MODE` is `true`, it takes precedence and disables all write operations regardless of `READ_ONLY_QUERY_MODE` setting. This is the recommended safe default to prevent inadvertent data modifications by LLMs.
166
220
 
167
221
  > Note: For authentication, you need either the Username and Password or the Client Certificate and key paths. Optionally, you can specify the CA root certificate path that will be used to validate the server certificates.
168
222
  > If both the Client Certificate & key path and the username and password are specified, the client certificates will be used for authentication.
169
223
 
224
+ ### Disabling Tools
225
+
226
+ You can disable specific tools to prevent them from being loaded and exposed to the MCP client. Disabled tools will not appear in the tool discovery and cannot be invoked by the LLM.
227
+
228
+ #### Supported Formats
229
+
230
+ **Comma-separated list:**
231
+
232
+ ```bash
233
+ # Environment variable
234
+ CB_MCP_DISABLED_TOOLS="upsert_document_by_id, delete_document_by_id"
235
+
236
+ # Command line
237
+ uvx couchbase-mcp-server --disabled-tools upsert_document_by_id, delete_document_by_id
238
+ ```
239
+
240
+ **File path (one tool name per line):**
241
+
242
+ ```bash
243
+ # Environment variable
244
+ CB_MCP_DISABLED_TOOLS=disabled_tools.txt
245
+
246
+ # Command line
247
+ uvx couchbase-mcp-server --disabled-tools disabled_tools.txt
248
+ ```
249
+
250
+ **File format (e.g., `disabled_tools.txt`):**
251
+
252
+ ```text
253
+ # Write operations
254
+ upsert_document_by_id
255
+ delete_document_by_id
256
+
257
+ # Index advisor
258
+ get_index_advisor_recommendations
259
+ ```
260
+
261
+ Lines starting with `#` are treated as comments and ignored.
262
+
263
+ #### MCP Client Configuration Examples
264
+
265
+ **Using comma-separated list:**
266
+
267
+ ```json
268
+ {
269
+ "mcpServers": {
270
+ "couchbase": {
271
+ "command": "uvx",
272
+ "args": ["couchbase-mcp-server"],
273
+ "env": {
274
+ "CB_CONNECTION_STRING": "couchbases://connection-string",
275
+ "CB_USERNAME": "username",
276
+ "CB_PASSWORD": "password",
277
+ "CB_MCP_DISABLED_TOOLS": "upsert_document_by_id,delete_document_by_id"
278
+ }
279
+ }
280
+ }
281
+ }
282
+ ```
283
+
284
+ **Using file path (recommended for many tools):**
285
+
286
+ ```json
287
+ {
288
+ "mcpServers": {
289
+ "couchbase": {
290
+ "command": "uvx",
291
+ "args": ["couchbase-mcp-server"],
292
+ "env": {
293
+ "CB_CONNECTION_STRING": "couchbases://connection-string",
294
+ "CB_USERNAME": "username",
295
+ "CB_PASSWORD": "password",
296
+ "CB_MCP_DISABLED_TOOLS": "/path/to/disabled_tools.txt"
297
+ }
298
+ }
299
+ }
300
+ }
301
+ ```
302
+
303
+ #### Important Security Note
304
+
305
+ > **Warning:** Disabling tools alone does not guarantee that certain operations cannot be performed. The underlying database user's RBAC (Role-Based Access Control) permissions are the authoritative security control.
306
+ >
307
+ > For example, even if you disable `upsert_document_by_id` and `delete_document_by_id`, data modifications can still occur via the `run_sql_plus_plus_query` tool using SQL++ DML statements (INSERT, UPDATE, DELETE, MERGE) unless:
308
+ > - The `CB_MCP_READ_ONLY_MODE` is set to `true` (default), OR
309
+ > - The database user lacks the necessary RBAC permissions for data modification
310
+ >
311
+ > **Best Practice:** Always configure appropriate RBAC permissions on your Couchbase user credentials as the primary security measure. Use tool disabling as an additional layer to guide LLM behavior and reduce the attack surface, not as the sole security control.
312
+
170
313
  You can also check the version of the server using:
171
314
 
172
315
  ```bash
173
316
  uvx couchbase-mcp-server --version
174
317
  ```
175
318
 
176
- #### Client Specific Configuration
319
+ ### Client Specific Configuration
177
320
 
178
321
  <details>
179
322
  <summary>Claude Desktop</summary>
@@ -248,6 +391,60 @@ For more details about MCP integration with Windsurf Editor, refer to the offici
248
391
 
249
392
  </details>
250
393
 
394
+ <details>
395
+ <summary>VS Code</summary>
396
+
397
+ Follow the steps below to use the Couchbase MCP server with [VS Code](https://code.visualstudio.com/).
398
+ 1. Install [VS Code](https://code.visualstudio.com/)
399
+ 2. Following are a couple of ways to configure the MCP server.
400
+ * For a Workspace server configuration
401
+ - Create a new file in workspace as .vscode/mcp.json.
402
+ - Add the [configuration](#configuration) and save the file.
403
+ * For the Global server configuration:
404
+ - Run **MCP: Open User Configuration** in the Command Pallete(`Ctrl+Shift+P` or `Cmd+Shift+P`)
405
+ - Add the [configuration](#configuration) and save the file.
406
+ * **Note**: VS Code uses `servers` as the top-level JSON property in mcp.json files to define MCP (Model Context Protocol) servers, while Cursor uses `mcpServers` for the equivalent configuration. Check the [VS Code client configurations](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) for any further changes or details. An example VS Code configuration is provided below.
407
+ ```json
408
+ {
409
+ "servers": {
410
+ "couchbase": {
411
+ "command": "uvx",
412
+ "args": ["couchbase-mcp-server"],
413
+ "env": {
414
+ "CB_CONNECTION_STRING": "couchbases://connection-string",
415
+ "CB_USERNAME": "username",
416
+ "CB_PASSWORD": "password"
417
+ }
418
+ }
419
+ }
420
+ }
421
+ ```
422
+ 3. Once you save the file, the server starts and a small action list appears with `Running|Stop|n Tools|More..`.
423
+ 4. Click on the options from the option list to `Start`/`Stop`/manage the server.
424
+ 5. You can now use the Couchbase MCP server in VS Code to query your Couchbase cluster using natural language and perform CRUD operations on documents.
425
+
426
+ Logs:
427
+ In the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P`),
428
+ - run **MCP: List Servers** command and pick the couchbase server
429
+ - choose “Show Output” to see its logs in the Output tab.
430
+ </details>
431
+
432
+ <details>
433
+ <summary>JetBrains IDEs</summary>
434
+
435
+ Follow the steps below to use the Couchbase MCP server with [JetBrains IDEs](https://www.jetbrains.com/)
436
+ 1. Install any one of the [JetBrains IDEs](https://www.jetbrains.com/)
437
+ 2. Install any one of the JetBrains plugins - [AI Assistant](https://www.jetbrains.com/help/ai-assistant/getting-started-with-ai-assistant.html) or [Junie](https://www.jetbrains.com/help/junie/get-started-with-junie.html)
438
+ 3. Navigate to **Settings > Tools > AI Assistant or Junie > MCP Server**
439
+ 4. Click "+" to add the Couchbase MCP [configuration](#configuration) and click Save.
440
+ 5. You will see the Couchbase MCP server added to the list of servers. Once you click Apply, the Couchbase MCP server starts and on-hover of status, it shows all the tools available.
441
+ 6. You can now use the Couchbase MCP server in JetBrains IDEs to query your Couchbase cluster using natural language and perform CRUD operations on documents.
442
+
443
+ Logs:
444
+ The log file can be explored at **Help > Show Log in Finder (Explorer) > mcp > couchbase**
445
+
446
+ </details>
447
+
251
448
  ## Streamable HTTP Transport Mode
252
449
 
253
450
  The MCP Server can be run in [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) transport mode which allows multiple clients to connect to the same server instance via HTTP.
@@ -264,7 +461,7 @@ uvx couchbase-mcp-server \
264
461
  --connection-string='<couchbase_connection_string>' \
265
462
  --username='<database_username>' \
266
463
  --password='<database_password>' \
267
- --read-only-query-mode=true \
464
+ --read-only-mode=true \
268
465
  --transport=http
269
466
  ```
270
467
 
@@ -297,7 +494,7 @@ uvx couchbase-mcp-server \
297
494
  --connection-string='<couchbase_connection_string>' \
298
495
  --username='<database_username>' \
299
496
  --password='<database_password>' \
300
- --read-only-query-mode=true \
497
+ --read-only-mode=true \
301
498
  --transport=sse
302
499
  ```
303
500
 
@@ -433,6 +630,21 @@ The Couchbase MCP server can also be used as a managed server in your agentic ap
433
630
  - Check the logs for any errors or warnings that may indicate issues with the MCP server. The location of the logs depend on your MCP client.
434
631
  - If you are observing issues running your MCP server from source after updating your local MCP server repository, try running `uv sync` to update the [dependencies](https://docs.astral.sh/uv/concepts/projects/sync/#syncing-the-environment).
435
632
 
633
+ ## Integration testing
634
+
635
+ We provide high-level MCP integration tests to verify that the server exposes the expected tools and that they can be invoked against a demo Couchbase cluster.
636
+
637
+ 1. Export demo cluster credentials:
638
+ - `CB_CONNECTION_STRING`
639
+ - `CB_USERNAME`
640
+ - `CB_PASSWORD`
641
+ - Optional: `CB_MCP_TEST_BUCKET` (a bucket to probe during the tests)
642
+ 2. Run the tests:
643
+
644
+ ```bash
645
+ uv run pytest tests/ -v
646
+ ```
647
+
436
648
  ---
437
649
 
438
650
  ## 👩‍💻 Contributing
@@ -0,0 +1,19 @@
1
+ certs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ certs/capella_root_ca.pem,sha256=SuSjgKclcQQg0kheTRd3dg6B0FUsUy717T5n3xcAU_E,1131
3
+ mcp_server.py,sha256=mI5Z_d1VTIdZnFirslUJW3UQnW8Ci-0RFyD90VyHhUg,6828
4
+ tools/__init__.py,sha256=7HNspBZLCpz5WwQkPg7p0irPYyrD3AouDUb6H0uzhvQ,3830
5
+ tools/index.py,sha256=cCBr0ptFBVc-HN5SoCauQAh2DsAP_Is8NPUSa6QcLM0,6682
6
+ tools/kv.py,sha256=ZRb7ixq3zCqiyPwJTtt4plNl9tCC_0kRDDqlUAh_apc,4938
7
+ tools/query.py,sha256=sOmn45nrysKlKavBymLSu-7sbwtFHBo0GwLcV-gmbDg,11552
8
+ tools/server.py,sha256=c5m_6GkwTaRwEelrQE4IHcs2aKZKd_X-BjnseMjkjMo,6734
9
+ utils/__init__.py,sha256=lV5QqPzMlHqInev9Nw_k3ToI9ABEZDfcqvE4p7cPLbU,1415
10
+ utils/config.py,sha256=4jIbE0dYHGidwlZVoLALNMLz_wBQ_J8XX5hvJPpu_BM,2730
11
+ utils/connection.py,sha256=NtAU4pmHMZubSJcs_X_lai9o8dih5mW0RyrRdmyp1Po,2892
12
+ utils/constants.py,sha256=w0zvQ1zMzJBg44Yl3aQW8KfaaRPn0BgPOLEe8xLeLSE,487
13
+ utils/context.py,sha256=q0eCe_I72rAEPWtSnf4qBEv5BuLHnko9qqCs4KTMW6g,2788
14
+ utils/index_utils.py,sha256=W0rvoBXU_2aB9m-HDlLChZoMzvlIX6FUWF6RTsYGfYM,10910
15
+ couchbase_mcp_server-0.6.0.dist-info/METADATA,sha256=3lUXRwa_fmDiXNHDj59oZaFePfsE5SzVFOTeESeJUEA,31086
16
+ couchbase_mcp_server-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
+ couchbase_mcp_server-0.6.0.dist-info/entry_points.txt,sha256=iU5pF4kIMTnNhoMPHhdH-k8o1Fmxb_iM9qJHCZcL6Ak,57
18
+ couchbase_mcp_server-0.6.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
19
+ couchbase_mcp_server-0.6.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
mcp_server.py CHANGED
@@ -10,7 +10,7 @@ import click
10
10
  from mcp.server.fastmcp import FastMCP
11
11
 
12
12
  # Import tools
13
- from tools import ALL_TOOLS
13
+ from tools import get_tools
14
14
 
15
15
  # Import utilities
16
16
  from utils import (
@@ -25,6 +25,7 @@ from utils import (
25
25
  NETWORK_TRANSPORTS_SDK_MAPPING,
26
26
  AppContext,
27
27
  get_settings,
28
+ parse_disabled_tools,
28
29
  )
29
30
 
30
31
  # Configure logging
@@ -41,14 +42,20 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
41
42
  """Initialize the MCP server context without establishing database connections."""
42
43
  # Get configuration from Click context
43
44
  settings = get_settings()
45
+ read_only_mode = settings.get("read_only_mode", True)
44
46
  read_only_query_mode = settings.get("read_only_query_mode", True)
45
47
 
46
48
  # Note: We don't validate configuration here to allow tool discovery
47
49
  # Configuration will be validated when tools are actually used
48
- logger.info("MCP server initialized in lazy mode for tool discovery.")
50
+ logger.info(
51
+ f"MCP server initialized in lazy mode for tool discovery. "
52
+ f"Modes: (read_only_mode={read_only_mode}, read_only_query_mode={read_only_query_mode})"
53
+ )
49
54
  app_context = None
50
55
  try:
51
- app_context = AppContext(read_only_query_mode=read_only_query_mode)
56
+ app_context = AppContext(
57
+ read_only_mode=read_only_mode, read_only_query_mode=read_only_query_mode
58
+ )
52
59
  yield app_context
53
60
 
54
61
  except Exception as e:
@@ -95,6 +102,13 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
95
102
  default=None,
96
103
  help="Path to the client certificate key file used for mTLS authentication.",
97
104
  )
105
+ @click.option(
106
+ "--read-only-mode",
107
+ envvar="CB_MCP_READ_ONLY_MODE",
108
+ type=bool,
109
+ default=DEFAULT_READ_ONLY_MODE,
110
+ help="Enable read-only mode. When True (default), all write operations (KV and Query) are disabled and KV write tools are not loaded. Set to False to enable write operations.",
111
+ )
98
112
  @click.option(
99
113
  "--read-only-query-mode",
100
114
  envvar=[
@@ -102,8 +116,9 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
102
116
  "READ_ONLY_QUERY_MODE", # Deprecated
103
117
  ],
104
118
  type=bool,
119
+ deprecated=True,
105
120
  default=DEFAULT_READ_ONLY_MODE,
106
- help="Enable read-only query mode. Set to True (default) to allow only read-only queries. Can be set to False to allow data modification queries.",
121
+ help="[DEPRECATED: Use --read-only-mode instead] Enable read-only query mode. Set to True (default) to allow only read-only queries. Can be set to False to allow data modification queries.",
107
122
  )
108
123
  @click.option(
109
124
  "--transport",
@@ -127,6 +142,13 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
127
142
  default=DEFAULT_PORT,
128
143
  help="Port to run the server on (default: 8000)",
129
144
  )
145
+ @click.option(
146
+ "--disabled-tools",
147
+ "disabled_tools",
148
+ envvar="CB_MCP_DISABLED_TOOLS",
149
+ help="Tools to disable. Accepts comma-separated tool names (e.g., 'tool_1,tool_2') "
150
+ "or a file path containing one tool name per line.",
151
+ )
130
152
  @click.version_option(package_name="couchbase-mcp-server")
131
153
  @click.pass_context
132
154
  def main(
@@ -137,10 +159,12 @@ def main(
137
159
  ca_cert_path,
138
160
  client_cert_path,
139
161
  client_key_path,
162
+ read_only_mode,
140
163
  read_only_query_mode,
141
164
  transport,
142
165
  host,
143
166
  port,
167
+ disabled_tools,
144
168
  ):
145
169
  """Couchbase MCP Server"""
146
170
  # Store configuration in context
@@ -151,12 +175,29 @@ def main(
151
175
  "ca_cert_path": ca_cert_path,
152
176
  "client_cert_path": client_cert_path,
153
177
  "client_key_path": client_key_path,
178
+ "read_only_mode": read_only_mode,
154
179
  "read_only_query_mode": read_only_query_mode,
155
180
  "transport": transport,
156
181
  "host": host,
157
182
  "port": port,
158
183
  }
159
184
 
185
+ # Get tools based on mode settings
186
+ # When read_only_mode is True, KV write tools are not loaded
187
+ tools = get_tools(read_only_mode=read_only_mode)
188
+
189
+ # Parse and validate disabled tools from CLI/environment variable
190
+ all_tool_names = {tool.__name__ for tool in tools}
191
+ disabled_tool_names = parse_disabled_tools(disabled_tools, all_tool_names)
192
+
193
+ if disabled_tool_names:
194
+ logger.info(
195
+ f"Disabled {len(disabled_tool_names)} tool(s): {sorted(disabled_tool_names)}"
196
+ )
197
+
198
+ # Filter out disabled tools
199
+ enabled_tools = [tool for tool in tools if tool.__name__ not in disabled_tool_names]
200
+
160
201
  # Map user-friendly transport names to SDK transport names
161
202
  sdk_transport = NETWORK_TRANSPORTS_SDK_MAPPING.get(transport, transport)
162
203
 
@@ -172,10 +213,17 @@ def main(
172
213
 
173
214
  mcp = FastMCP(MCP_SERVER_NAME, lifespan=app_lifespan, **config)
174
215
 
175
- # Register all tools
176
- for tool in ALL_TOOLS:
216
+ logger.info(
217
+ f"Registering {len(enabled_tools)} tool(s) with modes (read_only_mode={read_only_mode}, "
218
+ f"read_only_query_mode={read_only_query_mode})"
219
+ )
220
+
221
+ # Register only enabled tools
222
+ for tool in enabled_tools:
177
223
  mcp.add_tool(tool)
178
224
 
225
+ logger.info(f"Registered {len(enabled_tools)} tool(s)")
226
+
179
227
  # Run the server
180
228
  mcp.run(transport=sdk_transport) # type: ignore
181
229
 
tools/__init__.py CHANGED
@@ -2,8 +2,14 @@
2
2
  Couchbase MCP Tools
3
3
 
4
4
  This module contains all the MCP tools for Couchbase operations.
5
+
6
+ Tool Categories:
7
+ - READ_ONLY_TOOLS: Tools that only read data (always available)
8
+ - KV_WRITE_TOOLS: KV tools that modify data (disabled when READ_ONLY_MODE=True)
5
9
  """
6
10
 
11
+ from collections.abc import Callable
12
+
7
13
  # Index tools
8
14
  from .index import get_index_advisor_recommendations, list_indexes
9
15
 
@@ -11,11 +17,20 @@ from .index import get_index_advisor_recommendations, list_indexes
11
17
  from .kv import (
12
18
  delete_document_by_id,
13
19
  get_document_by_id,
20
+ insert_document_by_id,
21
+ replace_document_by_id,
14
22
  upsert_document_by_id,
15
23
  )
16
24
 
17
25
  # Query tools
18
26
  from .query import (
27
+ get_longest_running_queries,
28
+ get_most_frequent_queries,
29
+ get_queries_not_selective,
30
+ get_queries_not_using_covering_index,
31
+ get_queries_using_primary_index,
32
+ get_queries_with_large_result_count,
33
+ get_queries_with_largest_response_sizes,
19
34
  get_schema_for_collection,
20
35
  run_sql_plus_plus_query,
21
36
  )
@@ -31,24 +46,61 @@ from .server import (
31
46
  test_cluster_connection,
32
47
  )
33
48
 
34
- # List of all tools for easy registration
35
- ALL_TOOLS = [
49
+ # Read-only tools - always available regardless of mode settings
50
+ READ_ONLY_TOOLS = [
51
+ # Server/Cluster management tools
36
52
  get_buckets_in_cluster,
37
53
  get_server_configuration_status,
38
54
  test_cluster_connection,
39
55
  get_scopes_and_collections_in_bucket,
40
56
  get_collections_in_scope,
41
57
  get_scopes_in_bucket,
58
+ get_cluster_health_and_services,
59
+ # KV read tool
42
60
  get_document_by_id,
43
- upsert_document_by_id,
44
- delete_document_by_id,
61
+ # Query tools (read operations)
45
62
  get_schema_for_collection,
46
- run_sql_plus_plus_query,
63
+ run_sql_plus_plus_query, # Write protection handled at runtime via read_only_query_mode
64
+ # Index tools
47
65
  get_index_advisor_recommendations,
48
66
  list_indexes,
49
- get_cluster_health_and_services,
67
+ # Query performance analysis tools
68
+ get_queries_not_selective,
69
+ get_queries_not_using_covering_index,
70
+ get_queries_using_primary_index,
71
+ get_queries_with_large_result_count,
72
+ get_queries_with_largest_response_sizes,
73
+ get_longest_running_queries,
74
+ get_most_frequent_queries,
75
+ ]
76
+
77
+ # KV write tools - disabled when READ_ONLY_MODE is True
78
+ KV_WRITE_TOOLS = [
79
+ upsert_document_by_id,
80
+ insert_document_by_id,
81
+ replace_document_by_id,
82
+ delete_document_by_id,
50
83
  ]
51
84
 
85
+ # List of all tools for easy registration (kept for backward compatibility)
86
+ ALL_TOOLS = READ_ONLY_TOOLS + KV_WRITE_TOOLS
87
+
88
+
89
+ def get_tools(read_only_mode: bool = True) -> list[Callable]:
90
+ """Get the list of tools based on the mode settings.
91
+
92
+ This function determines which tools should be loaded based on the
93
+ READ_ONLY_MODE setting. When read_only_mode is True, write tools are excluded.
94
+ """
95
+ tools = list(READ_ONLY_TOOLS)
96
+
97
+ if not read_only_mode:
98
+ # KV write tools are only loaded when READ_ONLY_MODE is False
99
+ tools.extend(KV_WRITE_TOOLS)
100
+
101
+ return tools
102
+
103
+
52
104
  __all__ = [
53
105
  # Individual tools
54
106
  "get_server_configuration_status",
@@ -59,12 +111,25 @@ __all__ = [
59
111
  "get_buckets_in_cluster",
60
112
  "get_document_by_id",
61
113
  "upsert_document_by_id",
114
+ "insert_document_by_id",
115
+ "replace_document_by_id",
62
116
  "delete_document_by_id",
63
117
  "get_schema_for_collection",
64
118
  "run_sql_plus_plus_query",
65
119
  "get_index_advisor_recommendations",
66
120
  "list_indexes",
67
121
  "get_cluster_health_and_services",
122
+ "get_queries_not_selective",
123
+ "get_queries_not_using_covering_index",
124
+ "get_queries_using_primary_index",
125
+ "get_queries_with_large_result_count",
126
+ "get_queries_with_largest_response_sizes",
127
+ "get_longest_running_queries",
128
+ "get_most_frequent_queries",
129
+ # Tool categories
130
+ "READ_ONLY_TOOLS",
131
+ "KV_WRITE_TOOLS",
68
132
  # Convenience
69
133
  "ALL_TOOLS",
134
+ "get_tools",
70
135
  ]
tools/kv.py CHANGED
@@ -1,7 +1,12 @@
1
1
  """
2
2
  Tools for key-value operations.
3
3
 
4
- This module contains tools for getting a document by its ID, upserting a document by its ID, and deleting a document by its ID.
4
+ This module contains tools for document operations by ID:
5
+ - get: Retrieve a document
6
+ - upsert: Insert or update a document (creates if not exists, updates if exists)
7
+ - insert: Create a document only if it does NOT exist (fails if exists)
8
+ - replace: Update a document only if it exists (fails if missing)
9
+ - delete: Remove a document
5
10
  """
6
11
 
7
12
  import logging
@@ -46,6 +51,12 @@ def upsert_document_by_id(
46
51
  document_content: dict[str, Any],
47
52
  ) -> bool:
48
53
  """Insert or update a document by its ID.
54
+
55
+ IMPORTANT: Only use this tool when the user explicitly requests an 'upsert' operation
56
+ or explicitly states they want to 'insert or update' a document.
57
+
58
+ DO NOT use this as a fallback when insert_document_by_id or replace_document_by_id fails.
59
+
49
60
  Returns True on success, False on failure."""
50
61
  cluster = get_cluster_connection(ctx)
51
62
  bucket = connect_to_bucket(cluster, bucket_name)
@@ -78,3 +89,55 @@ def delete_document_by_id(
78
89
  except Exception as e:
79
90
  logger.error(f"Error deleting document {document_id}: {e}")
80
91
  return False
92
+
93
+
94
+ def insert_document_by_id(
95
+ ctx: Context,
96
+ bucket_name: str,
97
+ scope_name: str,
98
+ collection_name: str,
99
+ document_id: str,
100
+ document_content: dict[str, Any],
101
+ ) -> bool:
102
+ """Insert a new document by its ID. This operation will FAIL if the document already exists.
103
+
104
+ IMPORTANT: If this operation fails, DO NOT automatically try replace or upsert.
105
+ Report the failure to the user. They can choose to 'replace' or 'upsert' if desired.
106
+
107
+ Returns True on success, False on failure (including if document already exists)."""
108
+ cluster = get_cluster_connection(ctx)
109
+ bucket = connect_to_bucket(cluster, bucket_name)
110
+ try:
111
+ collection = bucket.scope(scope_name).collection(collection_name)
112
+ collection.insert(document_id, document_content)
113
+ logger.info(f"Successfully inserted document {document_id}")
114
+ return True
115
+ except Exception as e:
116
+ logger.error(f"Error inserting document {document_id}: {e}")
117
+ return False
118
+
119
+
120
+ def replace_document_by_id(
121
+ ctx: Context,
122
+ bucket_name: str,
123
+ scope_name: str,
124
+ collection_name: str,
125
+ document_id: str,
126
+ document_content: dict[str, Any],
127
+ ) -> bool:
128
+ """Replace an existing document by its ID. This operation will FAIL if the document does not exist.
129
+
130
+ IMPORTANT: If this operation fails, DO NOT automatically try insert or upsert.
131
+ Report the failure to the user. They can choose to 'insert' or 'upsert' if desired.
132
+
133
+ Returns True on success, False on failure (including if document does not exist)."""
134
+ cluster = get_cluster_connection(ctx)
135
+ bucket = connect_to_bucket(cluster, bucket_name)
136
+ try:
137
+ collection = bucket.scope(scope_name).collection(collection_name)
138
+ collection.replace(document_id, document_content)
139
+ logger.info(f"Successfully replaced document {document_id}")
140
+ return True
141
+ except Exception as e:
142
+ logger.error(f"Error replacing document {document_id}: {e}")
143
+ return False
tools/query.py CHANGED
@@ -39,21 +39,34 @@ def get_schema_for_collection(
39
39
  def run_sql_plus_plus_query(
40
40
  ctx: Context, bucket_name: str, scope_name: str, query: str
41
41
  ) -> list[dict[str, Any]]:
42
- """Run a SQL++ query on a scope and return the results as a list of JSON objects."""
42
+ """Run a SQL++ query on a scope and return the results as a list of JSON objects.
43
+
44
+ The query will be run on the specified scope in the specified bucket.
45
+ The query should use collection names directly without bucket/scope prefixes, as the scope context is automatically set.
46
+
47
+ Example:
48
+ query = "SELECT * FROM users WHERE age > 18"
49
+ # Incorrect: "SELECT * FROM bucket.scope.users WHERE age > 18"
50
+ """
43
51
  cluster = get_cluster_connection(ctx)
44
52
 
45
53
  bucket = connect_to_bucket(cluster, bucket_name)
46
54
 
47
55
  app_context = ctx.request_context.lifespan_context
56
+ read_only_mode = app_context.read_only_mode
48
57
  read_only_query_mode = app_context.read_only_query_mode
49
- logger.info(f"Running SQL++ queries in read-only mode: {read_only_query_mode}")
58
+
59
+ # Block query writes if either read_only_mode OR read_only_query_mode is True
60
+ # READ_ONLY_MODE takes precedence and blocks all writes (KV and Query)
61
+ # READ_ONLY_QUERY_MODE (deprecated) only blocks query writes
62
+ block_query_writes = read_only_mode or read_only_query_mode
50
63
 
51
64
  try:
52
65
  scope = bucket.scope(scope_name)
53
66
 
54
67
  results = []
55
68
  # If read-only mode is enabled, check if the query is a data or structure modification query
56
- if read_only_query_mode:
69
+ if block_query_writes:
57
70
  parsed_query = parse_sqlpp(query)
58
71
  data_modification_query = modifies_data(parsed_query)
59
72
  structure_modification_query = modifies_structure(parsed_query)
@@ -81,7 +94,6 @@ def run_sql_plus_plus_query(
81
94
  raise
82
95
 
83
96
 
84
- # Don't expose this function to the MCP server until we have a use case
85
97
  def run_cluster_query(ctx: Context, query: str, **kwargs: Any) -> list[dict[str, Any]]:
86
98
  """Run a query on the cluster object and return the results as a list of JSON objects."""
87
99
 
@@ -96,3 +108,258 @@ def run_cluster_query(ctx: Context, query: str, **kwargs: Any) -> list[dict[str,
96
108
  except Exception as e:
97
109
  logger.error(f"Error running query: {e}")
98
110
  raise
111
+
112
+
113
+ def _run_query_tool_with_empty_message(
114
+ ctx: Context,
115
+ query: str,
116
+ *,
117
+ limit: int,
118
+ empty_message: str,
119
+ extra_payload: dict[str, Any] | None = None,
120
+ **query_kwargs: Any,
121
+ ) -> list[dict[str, Any]]:
122
+ """Execute a cluster query with a consistent empty-result response."""
123
+ results = run_cluster_query(ctx, query, limit=limit, **query_kwargs)
124
+
125
+ if results:
126
+ return results
127
+
128
+ payload: dict[str, Any] = {"message": empty_message, "results": []}
129
+ if extra_payload:
130
+ payload.update(extra_payload)
131
+ return [payload]
132
+
133
+
134
+ def get_longest_running_queries(ctx: Context, limit: int = 10) -> list[dict[str, Any]]:
135
+ """Get the N longest running queries from the system:completed_requests catalog.
136
+
137
+ Args:
138
+ limit: Number of queries to return (default: 10)
139
+
140
+ Returns:
141
+ List of queries with their average service time and count
142
+ """
143
+ query = """
144
+ SELECT statement,
145
+ DURATION_TO_STR(avgServiceTime) AS avgServiceTime,
146
+ COUNT(1) AS queries
147
+ FROM system:completed_requests
148
+ WHERE UPPER(statement) NOT LIKE 'INFER %'
149
+ AND UPPER(statement) NOT LIKE 'CREATE INDEX%'
150
+ AND UPPER(statement) NOT LIKE 'CREATE PRIMARY INDEX%'
151
+ AND UPPER(statement) NOT LIKE '% SYSTEM:%'
152
+ GROUP BY statement
153
+ LETTING avgServiceTime = AVG(STR_TO_DURATION(serviceTime))
154
+ ORDER BY avgServiceTime DESC
155
+ LIMIT $limit
156
+ """
157
+
158
+ return _run_query_tool_with_empty_message(
159
+ ctx,
160
+ query,
161
+ limit=limit,
162
+ empty_message=(
163
+ "No completed queries were available to calculate longest running queries."
164
+ ),
165
+ )
166
+
167
+
168
+ def get_most_frequent_queries(ctx: Context, limit: int = 10) -> list[dict[str, Any]]:
169
+ """Get the N most frequent queries from the system:completed_requests catalog.
170
+
171
+ Args:
172
+ limit: Number of queries to return (default: 10)
173
+
174
+ Returns:
175
+ List of queries with their frequency count
176
+ """
177
+ query = """
178
+ SELECT statement,
179
+ COUNT(1) AS queries
180
+ FROM system:completed_requests
181
+ WHERE UPPER(statement) NOT LIKE 'INFER %'
182
+ AND UPPER(statement) NOT LIKE 'CREATE INDEX%'
183
+ AND UPPER(statement) NOT LIKE 'CREATE PRIMARY INDEX%'
184
+ AND UPPER(statement) NOT LIKE 'EXPLAIN %'
185
+ AND UPPER(statement) NOT LIKE 'ADVISE %'
186
+ AND UPPER(statement) NOT LIKE '% SYSTEM:%'
187
+ GROUP BY statement
188
+ LETTING queries = COUNT(1)
189
+ ORDER BY queries DESC
190
+ LIMIT $limit
191
+ """
192
+
193
+ return _run_query_tool_with_empty_message(
194
+ ctx,
195
+ query,
196
+ limit=limit,
197
+ empty_message=(
198
+ "No completed queries were available to calculate most frequent queries."
199
+ ),
200
+ )
201
+
202
+
203
+ def get_queries_with_largest_response_sizes(
204
+ ctx: Context, limit: int = 10
205
+ ) -> list[dict[str, Any]]:
206
+ """Get queries with the largest response sizes from the system:completed_requests catalog.
207
+
208
+ Args:
209
+ limit: Number of queries to return (default: 10)
210
+
211
+ Returns:
212
+ List of queries with their average result size in bytes, KB, and MB
213
+ """
214
+ query = """
215
+ SELECT statement,
216
+ avgResultSize AS avgResultSizeBytes,
217
+ (avgResultSize / 1000) AS avgResultSizeKB,
218
+ (avgResultSize / 1000000) AS avgResultSizeMB,
219
+ COUNT(1) AS queries
220
+ FROM system:completed_requests
221
+ WHERE UPPER(statement) NOT LIKE 'INFER %'
222
+ AND UPPER(statement) NOT LIKE 'CREATE INDEX%'
223
+ AND UPPER(statement) NOT LIKE 'CREATE PRIMARY INDEX%'
224
+ AND UPPER(statement) NOT LIKE '% SYSTEM:%'
225
+ GROUP BY statement
226
+ LETTING avgResultSize = AVG(resultSize)
227
+ ORDER BY avgResultSize DESC
228
+ LIMIT $limit
229
+ """
230
+
231
+ return _run_query_tool_with_empty_message(
232
+ ctx,
233
+ query,
234
+ limit=limit,
235
+ empty_message=(
236
+ "No completed queries were available to calculate response sizes."
237
+ ),
238
+ )
239
+
240
+
241
+ def get_queries_with_large_result_count(
242
+ ctx: Context, limit: int = 10
243
+ ) -> list[dict[str, Any]]:
244
+ """Get queries with the largest result counts from the system:completed_requests catalog.
245
+
246
+ Args:
247
+ limit: Number of queries to return (default: 10)
248
+
249
+ Returns:
250
+ List of queries with their average result count
251
+ """
252
+ query = """
253
+ SELECT statement,
254
+ avgResultCount,
255
+ COUNT(1) AS queries
256
+ FROM system:completed_requests
257
+ WHERE UPPER(statement) NOT LIKE 'INFER %' AND
258
+ UPPER(statement) NOT LIKE 'CREATE INDEX%' AND
259
+ UPPER(statement) NOT LIKE 'CREATE PRIMARY INDEX%' AND
260
+ UPPER(statement) NOT LIKE '% SYSTEM:%'
261
+ GROUP BY statement
262
+ LETTING avgResultCount = AVG(resultCount)
263
+ ORDER BY avgResultCount DESC
264
+ LIMIT $limit
265
+ """
266
+
267
+ return _run_query_tool_with_empty_message(
268
+ ctx,
269
+ query,
270
+ limit=limit,
271
+ empty_message=(
272
+ "No completed queries were available to calculate result counts."
273
+ ),
274
+ )
275
+
276
+
277
+ def get_queries_using_primary_index(
278
+ ctx: Context, limit: int = 10
279
+ ) -> list[dict[str, Any]]:
280
+ """Get queries that use a primary index from the system:completed_requests catalog.
281
+
282
+ Args:
283
+ limit: Number of queries to return (default: 10)
284
+
285
+ Returns:
286
+ List of queries that use primary indexes, ordered by result count
287
+ """
288
+ query = """
289
+ SELECT *
290
+ FROM system:completed_requests
291
+ WHERE phaseCounts.`primaryScan` IS NOT MISSING
292
+ AND UPPER(statement) NOT LIKE '% SYSTEM:%'
293
+ ORDER BY resultCount DESC
294
+ LIMIT $limit
295
+ """
296
+
297
+ return _run_query_tool_with_empty_message(
298
+ ctx,
299
+ query,
300
+ limit=limit,
301
+ empty_message=(
302
+ "No queries using the primary index were found in system:completed_requests."
303
+ ),
304
+ )
305
+
306
+
307
+ def get_queries_not_using_covering_index(
308
+ ctx: Context, limit: int = 10
309
+ ) -> list[dict[str, Any]]:
310
+ """Get queries that don't use a covering index from the system:completed_requests catalog.
311
+
312
+ Args:
313
+ limit: Number of queries to return (default: 10)
314
+
315
+ Returns:
316
+ List of queries that perform index scans but also require fetches (not covering)
317
+ """
318
+ query = """
319
+ SELECT *
320
+ FROM system:completed_requests
321
+ WHERE phaseCounts.`indexScan` IS NOT MISSING
322
+ AND phaseCounts.`fetch` IS NOT MISSING
323
+ AND UPPER(statement) NOT LIKE '% SYSTEM:%'
324
+ ORDER BY resultCount DESC
325
+ LIMIT $limit
326
+ """
327
+
328
+ return _run_query_tool_with_empty_message(
329
+ ctx,
330
+ query,
331
+ limit=limit,
332
+ empty_message=(
333
+ "No queries that require fetches after index scans were found "
334
+ "in system:completed_requests."
335
+ ),
336
+ )
337
+
338
+
339
+ def get_queries_not_selective(ctx: Context, limit: int = 10) -> list[dict[str, Any]]:
340
+ """Get queries that are not very selective from the system:completed_requests catalog.
341
+
342
+ Args:
343
+ limit: Number of queries to return (default: 10)
344
+
345
+ Returns:
346
+ List of queries where index scans return significantly more documents than the final result
347
+ """
348
+ query = """
349
+ SELECT statement,
350
+ AVG(phaseCounts.`indexScan` - resultCount) AS diff
351
+ FROM system:completed_requests
352
+ WHERE phaseCounts.`indexScan` > resultCount
353
+ GROUP BY statement
354
+ ORDER BY diff DESC
355
+ LIMIT $limit
356
+ """
357
+
358
+ return _run_query_tool_with_empty_message(
359
+ ctx,
360
+ query,
361
+ limit=limit,
362
+ empty_message=(
363
+ "No non-selective queries were found in system:completed_requests."
364
+ ),
365
+ )
tools/server.py CHANGED
@@ -29,6 +29,7 @@ def get_server_configuration_status(ctx: Context) -> dict[str, Any]:
29
29
  configuration = {
30
30
  "connection_string": settings.get("connection_string", "Not set"),
31
31
  "username": settings.get("username", "Not set"),
32
+ "read_only_mode": settings.get("read_only_mode", True),
32
33
  "read_only_query_mode": settings.get("read_only_query_mode", True),
33
34
  "password_configured": bool(settings.get("password")),
34
35
  "ca_cert_path_configured": bool(settings.get("ca_cert_path")),
utils/__init__.py CHANGED
@@ -7,6 +7,7 @@ This module contains utility functions for configuration, connection, and contex
7
7
  # Configuration utilities
8
8
  from .config import (
9
9
  get_settings,
10
+ parse_disabled_tools,
10
11
  )
11
12
 
12
13
  # Connection utilities
@@ -45,6 +46,7 @@ from .index_utils import (
45
46
  __all__ = [
46
47
  # Config
47
48
  "get_settings",
49
+ "parse_disabled_tools",
48
50
  # Connection
49
51
  "connect_to_couchbase_cluster",
50
52
  "connect_to_bucket",
utils/config.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from pathlib import Path
2
3
 
3
4
  import click
4
5
 
@@ -11,3 +12,78 @@ def get_settings() -> dict:
11
12
  """Get settings from Click context."""
12
13
  ctx = click.get_current_context()
13
14
  return ctx.obj or {}
15
+
16
+
17
+ def _parse_file(file_path: Path, valid_tool_names: set[str]) -> set[str]:
18
+ """Parse tool names from a file (one tool per line)."""
19
+ tools: set[str] = set()
20
+ invalid_count = 0
21
+ try:
22
+ with open(file_path) as f:
23
+ for raw_line in f:
24
+ name = raw_line.strip()
25
+ if not name or name.startswith("#"):
26
+ continue
27
+ if name in valid_tool_names:
28
+ tools.add(name)
29
+ else:
30
+ invalid_count += 1
31
+ if invalid_count > 0:
32
+ logger.warning(
33
+ f"Ignored {invalid_count} invalid tool name(s) from file: {file_path}"
34
+ )
35
+ logger.debug(f"Loaded {len(tools)} disabled tools from file: {file_path}")
36
+ except OSError as e:
37
+ logger.warning(f"Failed to read disabled tools file {file_path}: {e}")
38
+ return tools
39
+
40
+
41
+ def _parse_comma_separated(value: str, valid_tool_names: set[str]) -> set[str]:
42
+ """Parse comma-separated tool names."""
43
+ tools: set[str] = set()
44
+ invalid_count = 0
45
+ for part in value.split(","):
46
+ name = part.strip()
47
+ if name:
48
+ if name in valid_tool_names:
49
+ tools.add(name)
50
+ else:
51
+ invalid_count += 1
52
+ if invalid_count > 0:
53
+ logger.warning(
54
+ f"Ignored {invalid_count} invalid tool name(s) from comma-separated input"
55
+ )
56
+ logger.debug(f"Parsed disabled tools from comma-separated string: {tools}")
57
+ return tools
58
+
59
+
60
+ def parse_disabled_tools(
61
+ disabled_tools_input: str | None,
62
+ valid_tool_names: set[str],
63
+ ) -> set[str]:
64
+ """
65
+ Parse disabled tools from CLI argument or environment variable.
66
+
67
+ Supported formats:
68
+ 1. Comma-separated string: "tool_1,tool_2"
69
+ 2. File path containing one tool name per line: "disabled_tools.txt"
70
+
71
+ Args:
72
+ disabled_tools_input: Comma-separated tools or file path
73
+ valid_tool_names: Set of valid tool names to validate against
74
+
75
+ Returns:
76
+ Set of tool names to disable
77
+ """
78
+ if not disabled_tools_input:
79
+ return set()
80
+
81
+ value = disabled_tools_input.strip()
82
+
83
+ # Check if it's a file path
84
+ potential_path = Path(value)
85
+ if potential_path.exists() and potential_path.is_file():
86
+ return _parse_file(potential_path, valid_tool_names)
87
+
88
+ # Otherwise, treat as comma-separated
89
+ return _parse_comma_separated(value, valid_tool_names)
utils/context.py CHANGED
@@ -13,9 +13,19 @@ logger = logging.getLogger(f"{MCP_SERVER_NAME}.utils.context")
13
13
 
14
14
  @dataclass
15
15
  class AppContext:
16
- """Context for the MCP server."""
16
+ """Context for the MCP server.
17
+
18
+ Attributes:
19
+ cluster: The Couchbase cluster connection (lazily initialized).
20
+ read_only_mode: When True, all write operations (KV and Query) are disabled
21
+ and KV write tools are not loaded.
22
+ This is the recommended mode for safety. Default is True.
23
+ read_only_query_mode: When True, query-based write operations are disabled.
24
+ DEPRECATED: Use read_only_mode instead.
25
+ """
17
26
 
18
27
  cluster: Cluster | None = None
28
+ read_only_mode: bool = True
19
29
  read_only_query_mode: bool = True
20
30
 
21
31
 
@@ -1,19 +0,0 @@
1
- certs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- certs/capella_root_ca.pem,sha256=SuSjgKclcQQg0kheTRd3dg6B0FUsUy717T5n3xcAU_E,1131
3
- mcp_server.py,sha256=dvHQR-55JhuXX_bj2hLU0ek411qPp0qHEKqAmPdwjKU,4991
4
- tools/__init__.py,sha256=otQd8IiEILtl1jgorEO1E9KqL_2xBBlqwnVHejoCPE0,1668
5
- tools/index.py,sha256=cCBr0ptFBVc-HN5SoCauQAh2DsAP_Is8NPUSa6QcLM0,6682
6
- tools/kv.py,sha256=NGUs43iuXElj9rYe4RCyCStqoh5y1fUgbg1oWuU4WeQ,2493
7
- tools/query.py,sha256=fSSG7XuxnND1lHwI-r8y_M3h6f82vCXxCwB_A2wHN-M,3598
8
- tools/server.py,sha256=4krPjBoQmUHUmOE03T0CcCGFJtoaeHVS91oOrYj8JsA,6670
9
- utils/__init__.py,sha256=Fcbp-VIK0Gwwy7hl1AjV9NBKZsscmOV2vYdTBam5M3A,1361
10
- utils/config.py,sha256=B6H_JYDn6uxtu9juM924zdvNQgSaHh_u6rYME3_0_xQ,268
11
- utils/connection.py,sha256=NtAU4pmHMZubSJcs_X_lai9o8dih5mW0RyrRdmyp1Po,2892
12
- utils/constants.py,sha256=w0zvQ1zMzJBg44Yl3aQW8KfaaRPn0BgPOLEe8xLeLSE,487
13
- utils/context.py,sha256=XZL4M70BMdFBptJ9sT0zxhEey-EvvoSKZJrP_sb7q-A,2286
14
- utils/index_utils.py,sha256=W0rvoBXU_2aB9m-HDlLChZoMzvlIX6FUWF6RTsYGfYM,10910
15
- couchbase_mcp_server-0.5.2rc6.dist-info/METADATA,sha256=0WxUrZ50H8QFsVtw2Zy9Xp-rDvB34zVzvL3wxnai_0Q,21038
16
- couchbase_mcp_server-0.5.2rc6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- couchbase_mcp_server-0.5.2rc6.dist-info/entry_points.txt,sha256=iU5pF4kIMTnNhoMPHhdH-k8o1Fmxb_iM9qJHCZcL6Ak,57
18
- couchbase_mcp_server-0.5.2rc6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
19
- couchbase_mcp_server-0.5.2rc6.dist-info/RECORD,,