Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DOCKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ Documentation: <https://mcp-server.couchbase.com>
| Tool Name | Description |
| --------- | ----------- |
| `get_document_by_id` | Get a document by ID from a specified scope and collection |
| `sub_document_lookup_in` | Look up parts of a document (specific fields, existence checks, or array/object counts) by path without fetching the whole document |
| `lookup_subdocument` | Look up parts of a document (specific fields, existence checks, or array/object counts) by path without fetching the whole document |
| `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`.** |
| `insert_document_by_id` | Insert a new document by ID (fails if document exists). **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |
| `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`.** |
| `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`.** |
| `sub_document_mutate_in` | Modify parts of an existing document (upsert, insert, replace, remove, array ops, counters) by path without rewriting the whole document. **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |
| `mutate_subdocument` | Modify parts of an existing document (upsert, insert, replace, remove, array ops, counters) by path without rewriting the whole document. **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |

### Query and indexing tools

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ For full documentation, visit [mcp-server.couchbase.com](https://mcp-server.couc
| Tool Name | Description |
| --------- | ----------- |
| `get_document_by_id` | Get a document by ID from a specified scope and collection |
| `sub_document_lookup_in` | Look up parts of a document (specific fields, existence checks, or array/object counts) by path without fetching the whole document |
| `lookup_subdocument` | Look up parts of a document (specific fields, existence checks, or array/object counts) by path without fetching the whole document |
| `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`.** |
| `insert_document_by_id` | Insert a new document by ID (fails if document exists). **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |
| `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`.** |
| `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`.** |
| `sub_document_mutate_in` | Modify parts of an existing document (upsert, insert, replace, remove, array ops, counters) by path without rewriting the whole document. **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |
| `mutate_subdocument` | Modify parts of an existing document (upsert, insert, replace, remove, array ops, counters) by path without rewriting the whole document. **Disabled by default when `CB_MCP_READ_ONLY_MODE=true`.** |

### Query and indexing tools

Expand Down
16 changes: 8 additions & 8 deletions src/cb_mcp/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
delete_document_by_id,
get_document_by_id,
insert_document_by_id,
lookup_subdocument,
mutate_subdocument,
replace_document_by_id,
sub_document_lookup_in,
sub_document_mutate_in,
upsert_document_by_id,
)

Expand Down Expand Up @@ -80,7 +80,7 @@
get_cluster_health_and_services,
# KV read tools
get_document_by_id,
sub_document_lookup_in,
lookup_subdocument,
# Query tools (read operations)
get_schema_for_collection,
run_sql_plus_plus_query, # Write protection handled at runtime via read_only_mode
Expand All @@ -104,7 +104,7 @@
insert_document_by_id,
replace_document_by_id,
delete_document_by_id,
sub_document_mutate_in,
mutate_subdocument,
]

# Scope/collection management write tools - disabled when READ_ONLY_MODE is True
Expand Down Expand Up @@ -139,7 +139,7 @@
"get_cluster_health_and_services": ToolAnnotations(readOnlyHint=True),
# KV read tools
"get_document_by_id": ToolAnnotations(readOnlyHint=True),
"sub_document_lookup_in": ToolAnnotations(readOnlyHint=True),
"lookup_subdocument": ToolAnnotations(readOnlyHint=True),
# Query tools
"get_schema_for_collection": ToolAnnotations(readOnlyHint=True),
"run_sql_plus_plus_query": ToolAnnotations(),
Expand All @@ -160,7 +160,7 @@
"insert_document_by_id": ToolAnnotations(idempotentHint=True),
"replace_document_by_id": ToolAnnotations(idempotentHint=True),
"delete_document_by_id": ToolAnnotations(destructiveHint=True, idempotentHint=True),
"sub_document_mutate_in": ToolAnnotations(destructiveHint=True),
"mutate_subdocument": ToolAnnotations(destructiveHint=True),
# Scope/collection management write tools
"create_scope": ToolAnnotations(),
"create_collection": ToolAnnotations(),
Expand Down Expand Up @@ -199,8 +199,8 @@ def get_tools(read_only_mode: bool = True) -> list[Callable]:
"get_scopes_in_bucket",
"get_buckets_in_cluster",
"get_document_by_id",
"sub_document_lookup_in",
"sub_document_mutate_in",
"lookup_subdocument",
"mutate_subdocument",
"upsert_document_by_id",
"insert_document_by_id",
"replace_document_by_id",
Expand Down
10 changes: 5 additions & 5 deletions src/cb_mcp/tools/kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def replace_document_by_id(
return tool_error(e)


def sub_document_lookup_in(
def lookup_subdocument(
ctx: Context,
bucket_name: str,
scope_name: str,
Expand Down Expand Up @@ -270,7 +270,7 @@ def sub_document_lookup_in(
return response


def sub_document_mutate_in(
def mutate_subdocument(
ctx: Context,
bucket_name: str,
scope_name: str,
Expand All @@ -293,12 +293,12 @@ def sub_document_mutate_in(

Use this instead of upsert_document_by_id/replace_document_by_id when you only need to
change, add, or remove a few fields — AND you already know the exact field path(s) to
mutate (e.g. from a prior get_document_by_id or sub_document_lookup_in call, from the
mutate (e.g. from a prior get_document_by_id or lookup_subdocument call, from the
user explicitly naming the field, or from a known schema). Do NOT guess field paths.

IMPORTANT — atomicity: a mutate_in call is all-or-nothing. If ANY requested spec fails
(e.g. an insert on a path that already exists), the ENTIRE call fails and NONE of the
mutations are applied — unlike sub_document_lookup_in, there is no partial success.
mutations are applied — unlike lookup_subdocument, there is no partial success.

Provide one or more of the following lists. Each path uses Couchbase's dot/bracket path
syntax (e.g. "address.city", "tags[0]", "tags[-1]" for the last array element):
Expand Down Expand Up @@ -461,7 +461,7 @@ def sub_document_mutate_in(
try:
new_value = read_new_value()
break
except Exception:
except Exception: # noqa: S112 (expected fallback attempt, not a swallowed bug)
continue

if new_value is None:
Expand Down
28 changes: 14 additions & 14 deletions tests/accuracy/result_validation/test_kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
returns success/failure and the answer must report that outcome correctly
without fabricating data. A non-existent get must not hallucinate.

sub_document_lookup_in cases mirror the get cases (exact seeded value, exact
lookup_subdocument cases mirror the get cases (exact seeded value, exact
count, no hallucination on a missing field) but ask for field-existence,
value, and array-count questions that only need a sub-document read.

sub_document_mutate_in cases are faithfulness checks like the write cases
mutate_subdocument cases are faithfulness checks like the write cases
above: the tool mutates one field, and the answer must report the outcome
without contradicting it — verified by reading the field back afterward.

Expand Down Expand Up @@ -219,7 +219,7 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[ResultCase]:
)
)

# --- sub_document_lookup_in: exists + value (seeded) -----------------
# --- lookup_subdocument: exists + value (seeded) -----------------
subdoc_exists_id = doc_id("rv_subdoc_exists")
cases.append(
ResultCase(
Expand All @@ -246,7 +246,7 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[ResultCase]:
)
)

# --- sub_document_lookup_in: count (seeded) ---------------------------
# --- lookup_subdocument: count (seeded) ---------------------------
subdoc_count_id = doc_id("rv_subdoc_count")
cases.append(
ResultCase(
Expand Down Expand Up @@ -274,7 +274,7 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[ResultCase]:
)
)

# --- sub_document_lookup_in: missing field -> must NOT hallucinate ---
# --- lookup_subdocument: missing field -> must NOT hallucinate ---
subdoc_missing_id = doc_id("rv_subdoc_missing")
cases.append(
ResultCase(
Expand Down Expand Up @@ -302,11 +302,11 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[ResultCase]:
)
)

# --- sub_document_mutate_in: upsert a single field (faithfulness) ----
# --- mutate_subdocument: upsert a single field (faithfulness) ----
mutate_upsert_id = doc_id("rv_mutate_upsert")
cases.append(
ResultCase(
test_id="sub_document_mutate_in_upsert_reports_success",
test_id="mutate_subdocument_upsert_reports_success",
prompt=(
f"On document '{mutate_upsert_id}' in bucket '{bucket}', scope "
f"'{scope}', collection '{collection}', set its 'status' field "
Expand All @@ -327,11 +327,11 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[ResultCase]:
)
)

# --- sub_document_mutate_in: counter (seeded ground truth) -----------
# --- mutate_subdocument: counter (seeded ground truth) -----------
mutate_counter_id = doc_id("rv_mutate_counter")
cases.append(
ResultCase(
test_id="sub_document_mutate_in_counter_reports_new_value",
test_id="mutate_subdocument_counter_reports_new_value",
prompt=(
f"Increment the 'views' field of document '{mutate_counter_id}' "
f"in bucket '{bucket}', scope '{scope}', collection '{collection}' "
Expand All @@ -353,11 +353,11 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[ResultCase]:
)
)

# --- sub_document_mutate_in: remove a field (faithfulness) -----------
# --- mutate_subdocument: remove a field (faithfulness) -----------
mutate_remove_id = doc_id("rv_mutate_remove")
cases.append(
ResultCase(
test_id="sub_document_mutate_in_remove_reports_success",
test_id="mutate_subdocument_remove_reports_success",
prompt=(
f"Remove the 'temp_note' field from document '{mutate_remove_id}' "
f"in bucket '{bucket}', scope '{scope}', collection '{collection}'."
Expand Down Expand Up @@ -398,9 +398,9 @@ def kv_cases(test_bucket: str, test_scope: str, test_collection: str):
"sub_document_exists_and_value",
"sub_document_count",
"sub_document_missing_field_no_hallucination",
"sub_document_mutate_in_upsert_reports_success",
"sub_document_mutate_in_counter_reports_new_value",
"sub_document_mutate_in_remove_reports_success",
"mutate_subdocument_upsert_reports_success",
"mutate_subdocument_counter_reports_new_value",
"mutate_subdocument_remove_reports_success",
]


Expand Down
40 changes: 20 additions & 20 deletions tests/accuracy/tool_calling/test_kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
- get / insert / upsert / replace / delete (one each)
- multi-step (get → upsert)
- negative selection (a "read-only" prompt must not call delete)
- sub_document_lookup_in selected over get_document_by_id for exists/count/
- lookup_subdocument selected over get_document_by_id for exists/count/
single-field prompts
- sub_document_mutate_in selected over upsert_document_by_id for single-field
- mutate_subdocument selected over upsert_document_by_id for single-field
set/append/increment prompts
"""

Expand Down Expand Up @@ -252,15 +252,15 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[AccuracyCase]
exists_id = _doc_id("acc_subdoc_exists")
cases.append(
AccuracyCase(
test_id="sub_document_lookup_in_exists",
test_id="lookup_subdocument_exists",
prompt=(
f"Without fetching its full contents, check whether document "
f"'{exists_id}' in bucket '{bucket}', scope '{scope}', collection "
f"'{collection}' has a field called 'nickname'."
),
expected_tools=[
ExpectedToolCall(
tool_name="sub_document_lookup_in",
tool_name="lookup_subdocument",
parameters={
"bucket_name": bucket,
"scope_name": scope,
Expand All @@ -280,15 +280,15 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[AccuracyCase]
count_id = _doc_id("acc_subdoc_count")
cases.append(
AccuracyCase(
test_id="sub_document_lookup_in_count",
test_id="lookup_subdocument_count",
prompt=(
f"How many items are in the 'tags' array of document '{count_id}' "
f"in bucket '{bucket}', scope '{scope}', collection '{collection}'? "
"Don't fetch the whole document."
),
expected_tools=[
ExpectedToolCall(
tool_name="sub_document_lookup_in",
tool_name="lookup_subdocument",
parameters={
"bucket_name": bucket,
"scope_name": scope,
Expand All @@ -312,15 +312,15 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[AccuracyCase]
field_id = _doc_id("acc_subdoc_get")
cases.append(
AccuracyCase(
test_id="sub_document_lookup_in_get_field_not_whole_doc",
test_id="lookup_subdocument_get_field_not_whole_doc",
prompt=(
f"I only need the 'city' field nested under 'address' in document "
f"'{field_id}' (bucket '{bucket}', scope '{scope}', collection "
f"'{collection}'). Don't fetch the whole document, just that field."
),
expected_tools=[
ExpectedToolCall(
tool_name="sub_document_lookup_in",
tool_name="lookup_subdocument",
parameters={
"bucket_name": bucket,
"scope_name": scope,
Expand All @@ -344,7 +344,7 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[AccuracyCase]
mutate_upsert_id = _doc_id("acc_mutate_upsert")
cases.append(
AccuracyCase(
test_id="sub_document_mutate_in_upsert_single_field",
test_id="mutate_subdocument_upsert_single_field",
prompt=(
f"On document '{mutate_upsert_id}' in bucket '{bucket}', scope "
f"'{scope}', collection '{collection}', just set its 'status' "
Expand All @@ -353,7 +353,7 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[AccuracyCase]
),
expected_tools=[
ExpectedToolCall(
tool_name="sub_document_mutate_in",
tool_name="mutate_subdocument",
parameters={
"bucket_name": bucket,
"scope_name": scope,
Expand All @@ -373,15 +373,15 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[AccuracyCase]
mutate_append_id = _doc_id("acc_mutate_append")
cases.append(
AccuracyCase(
test_id="sub_document_mutate_in_array_append",
test_id="mutate_subdocument_array_append",
prompt=(
f"Append the value 'urgent' to the 'tags' array of document "
f"'{mutate_append_id}' in bucket '{bucket}', scope '{scope}', "
f"collection '{collection}'. Only modify that array field."
),
expected_tools=[
ExpectedToolCall(
tool_name="sub_document_mutate_in",
tool_name="mutate_subdocument",
parameters={
"bucket_name": bucket,
"scope_name": scope,
Expand All @@ -405,15 +405,15 @@ def _build_cases(bucket: str, scope: str, collection: str) -> list[AccuracyCase]
mutate_counter_id = _doc_id("acc_mutate_counter")
cases.append(
AccuracyCase(
test_id="sub_document_mutate_in_counter",
test_id="mutate_subdocument_counter",
prompt=(
f"Increment the 'views' counter of document '{mutate_counter_id}' "
f"in bucket '{bucket}', scope '{scope}', collection '{collection}' "
"by 3. Only modify that field."
),
expected_tools=[
ExpectedToolCall(
tool_name="sub_document_mutate_in",
tool_name="mutate_subdocument",
parameters={
"bucket_name": bucket,
"scope_name": scope,
Expand Down Expand Up @@ -451,12 +451,12 @@ def kv_cases(test_bucket: str, test_scope: str, test_collection: str):
"get_then_upsert_multistep",
"read_only_prompt_uses_get_only",
"conversational_lookup_document",
"sub_document_lookup_in_exists",
"sub_document_lookup_in_count",
"sub_document_lookup_in_get_field_not_whole_doc",
"sub_document_mutate_in_upsert_single_field",
"sub_document_mutate_in_array_append",
"sub_document_mutate_in_counter",
"lookup_subdocument_exists",
"lookup_subdocument_count",
"lookup_subdocument_get_field_not_whole_doc",
"mutate_subdocument_upsert_single_field",
"mutate_subdocument_array_append",
"mutate_subdocument_counter",
]


Expand Down
8 changes: 4 additions & 4 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@
"get_collections_in_scope",
"get_scopes_in_bucket",
"get_document_by_id",
"sub_document_lookup_in",
"sub_document_mutate_in",
"lookup_subdocument",
"mutate_subdocument",
"upsert_document_by_id",
"insert_document_by_id",
"replace_document_by_id",
Expand Down Expand Up @@ -98,8 +98,8 @@
},
"kv": {
"get_document_by_id",
"sub_document_lookup_in",
"sub_document_mutate_in",
"lookup_subdocument",
"mutate_subdocument",
"upsert_document_by_id",
"insert_document_by_id",
"replace_document_by_id",
Expand Down
Loading