Skip to content

RCV V3 - workflow based tools - #425

Open
gsharini wants to merge 24 commits into
oracle:mainfrom
gsharini:rcv_v3
Open

gsharini wants to merge 24 commits into
oracle:mainfrom
gsharini:rcv_v3

Conversation

@gsharini

Copy link
Copy Markdown
Member

This change delivers Recovery MCP Server v3.0.0, expanded Recovery Service and Database Service read coverage, and guided operational workflows for Cloud Protect onboarding.

Fixes # (issue)

Type of change
Please delete options that are not relevant.

Bug fix (non-breaking change which fixes an issue)
New feature (non-breaking change which adds functionality)
Breaking change (fix or feature that would cause existing functionality to not work as expected)
This change requires a documentation update
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

Added targeted unit tests covering:
Guidance-tool availability and input validation.

Onboard testing :
Prompt
codex> "Using recovery service mcp server onboard database DB0729 with ip 100.102.44.215 to recovery service."

Final Result:
DB0729 is onboarded to Recovery Service.

Protected database: db0729_xxm_iad
Status: ACTIVE
Policy: Bronze, 14-day retention
Recovery Service subnet: the OCID you supplied
Scheduled Cloud Protect backup task: every 15 minutes
The initial health is WARNING — Waiting for archive logs, which is expected immediately after onboarding; the first scheduled task is due at 13:20 UTC. Real-time redo remains disabled. I did not enable it because that is a separate configuration change and requires a
Checklist:
My code follows the style guidelines of this project
I have performed a self-review of my own code
I have commented my code, particularly in hard-to-understand areas
I have made corresponding changes to the documentation
My changes generate no new warnings
I have added tests that prove my fix is effective or that my feature works
New and existing unit tests pass locally with my changes
Any dependent changes have been merged and published in downstream modules

@oracle-contributor-agreement oracle-contributor-agreement Bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Aug 26, 2026
arampuri and others added 2 commits August 27, 2026 01:11
… 0.1.3

The rcv_v3 merge kept the recovery server's uv.lock from the feature
branch, so it still pinned the oracle-mcp-common workspace member at
0.1.2 while src/common had been bumped to 0.1.3 by oracle#421.

This made the `Sync` step (`uv sync --locked --all-extras --dev`) fail
in build (oci-recovery-mcp-server), which cancelled the rest of the
build matrix.

@dustin-sale dustin-sale left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by @dustin-sale via Codex.

Requested changes

No blocking changes requested.

Additional review notes

  • src/oci-recovery-mcp-server/README.md:100 — [P2] Correct the version-pinned uvx example.

Validation

  • make lint and make test project=oci-recovery-mcp-server passed.
  • The packaged wheel includes the new prompt files.

See the inline comments in this review for evidence, impact, and suggested remediation.

Comment thread src/oci-recovery-mcp-server/README.md Outdated
@@ -0,0 +1,2221 @@
"""
Copyright (c) 2025, 2026 Oracle and/or its affiliates.

@gebhardtr gebhardtr Sep 2, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2000 lines of code in this module; are they all necessary? can we break this file up into specific categories of tests? is it named coverage because the stated purpose from the prompt was to increase test coverage?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix coming from gsharini#11

return request_id
installation_id = _mcp_installation_id()
actor_id = _MCP_ACTOR_ID_CONTEXT.get()[:_MCP_ACTOR_ID_LENGTH].ljust(_MCP_ACTOR_ID_LENGTH, "0")
tool_code = _MCP_TOOL_CODES.get(_MCP_TOOL_ID_CONTEXT.get(), "unk")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this?

@arampuri arampuri Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a small segment of code we have added to keep telemetry of API calls being made to our service via the mcp server.
By adding a prefix to opc-request-id.


Required:
- ORACLE_MCP_AUTH_METHOD: "session" or "apikey"
def _legacy_auth_type_override() -> Optional[AuthType]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this called "legacy auth"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are now using oci-commons auth and in the prior version we had implemented it in this service-space. To keep backward compatibility we this has been modified but its actually deprecated.
We will phase it in future commits.

for n in names:
v = os.getenv(n)
if v is not None and v.strip() != "":
return v.strip()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're calling v.strip() twice.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

if m in ("apikey", "api_key", "api-key"):
return "apikey"
return "session"
raw = (os.getenv("ORACLE_MCP_AUTH_METHOD") or "").strip().lower()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will:

OCI_MCP_AUTH_TYPE=security_token
ORACLE_MCP_AUTH_METHOD=apikey

result in rejected requests after successful authentication?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix coming in - gsharini#11


qualified = _qualify(audience, scopes)
provider.update_default_scopes(qualified)
provider.required_scopes = qualified

@gebhardtr gebhardtr Sep 2, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this changes the scopes enforced by RequireAuthMiddleware (FastMCP). IDCS returns a bare resource scope (not qualified), and the token verifier keeps that scope (from build_idcs_http_auth()).

This is another case where an authenticated session can result in request failures (insufficient_scope). There's two forms that have to be accounted for, and this falls through the gap between them: IDCS expects audience qualified in the requests but emits the bare form in the token scope claim (https://docs.oracle.com/en/cloud/saas/marketing/audience-develop/docs/authentication/authenticating-oauth.htm)

Can you confirm?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix comin gin - gsharini#11

@gebhardtr gebhardtr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comments

@gsharini

gsharini commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

gsharini#11
will be merged here which will address the above comments in addition to other service specific comments

arampuri and others added 3 commits September 3, 2026 19:01
if now - float(cached.get("fetched_at") or 0.0) >= ttl:
entries.pop(key, None)
return None
entries[key] = entries.pop(key)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't thread safe and fastmcp executes synchronous tools in worker threads.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix is in gsharini#12

# - We try to be resilient to SDK shape differences by using getattr/__dict__/to_dict
# wherever possible, especially for pagination and nested model fields.
# - We log key milestones and counts for better operability and diagnostics.

import configparser
import hashlib

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file is huge. can you break it up? I see a lot of different candidates for submodules in here (caching is one example).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix is in gsharini#12

_CACHE_MAX_ENTRIES = int(os.getenv("ORACLE_MCP_CACHE_MAX_ENTRIES", "256"))


def _cache_get(entries: dict[str, Any], key: str, *, ttl: float, now: float) -> Optional[Any]:

@gebhardtr gebhardtr Sep 8, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did we implement our our caching layer and code?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We originally built a custom cache to explore a different staleness and expiration algorithm. That work is now on hold pending internal review of its potential novelty. We have added the missing synchronization, but we can replace the current implementation with a standard TTL cache library if that is preferred for this PR. Our preference is to keep the cache implementation so the underlying strategy can be replaced without affecting the tools.

tenancy_id = get_tenancy()
cache_key = f"iam:list_region_subscriptions:{tenancy_id}"
cached = _cache_get(items, cache_key, ttl=ttl, now=now)
if cached:

@gebhardtr gebhardtr Sep 8, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache is consulted before authorization check happens below. This can result in someone being able to list region subscriptions even if they lose permission to do so (TENANCY_INSPECT).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For region caching, OCI performs authorization during the API call,  there is no separate local check to move before the cache lookup. Always calling OCI would ensure immediate authz check but would defeat our goal of preventing agents from repeatedly hitting production APIs. We therefore made the region cache caller-scoped, as our other caches already are, and will use a short TTL to limit permission staleness. ( list_**region was wrongly scoped to tenancy that has been fixed)

@@ -834,19 +1424,19 @@ def _iam_subscribed_regions_with_status(*, request_id: str) -> list[dict]:
Returns the tenancy's subscribed regions from IAM (IdentityClient.list_region_subscriptions).
Output items are: {"region": "<region_name>", "status": "<READY|...>"}.

Cached in-process for ORACLE_MCP_REGION_CACHE_TTL_SECONDS to avoid repeated IAM calls.
Cached in-process for ORACLE_MCP_REGION_CACHE_TTL_SECONDS, partitioned per tenant.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this comment changed? it still behaves this way:

to avoid repeated IAM calls

correct?

}


def get_compartment_by_name_tool(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused definition

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"""
if coll is None:
return None
data = _oci_to_dict(coll) or {}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few unused definitions in this file please take care.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed & cleaned up.

return resolved_id


def fetch_child_compartments(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused definition please take care

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the four unused functions.

)


def get_onesubscription_client(region: str | None = None, *, request_id: Optional[str] = None):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused definition

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

the client factories need to know about.
"""

import logging

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few unused imports here please take care.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned up.


"""MCP tools available in this server:
- fetch_regions_subscribed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we further split this file based on tools? May be one module per tool family?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. server.py is now 94 lines (module docstring + main() + registration imports). Split into recovery_tools.py (12 tools), database_tools.py (6), summarise_tools.py (4), prompt_tools.py (3), plus app.py holding the FastMCP instance and shared tool constants - that last one exists to break the import cycle, since the family modules need mcp to register and server.py needs the families imported. All 25 tools still register; verified by comparing the registered tool list bef


def get_compartment_by_name(compartment_name: str):
"""Internal function to get compartment by name with caching"""
compartments = list_all_compartments_internal(False)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to use cache here or directly call OCI ?
docstring mentions cache but code directly pulls from OCI

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - the docstring was right about the intent and the code wasn't. get_compartment_by_name now goes through _list_all_compartments_cached().

"""
# An explicit override always wins. Over HTTP it is the only source: there is
# no local OCI config file on a hosted deployment to read a tenancy from.
override = _first_env("TENANCY_ID_OVERRIDE", "ORACLE_MCP_TENANCY_ID")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use ?
override = _first_env(
"OCI_MCP_TENANCY_ID_OVERRIDE", # canonical, matches oracle-mcp-common
"ORACLE_MCP_TENANCY_ID",
"TENANCY_ID_OVERRIDE",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied as suggested. Confirmed OCI_MCP_TENANCY_ID_OVERRIDE is the canonical name in oracle_mcp_common/auth.py:243; the other two stay as deprecated fallbacks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

region is ignored, can we honor this argument?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Precedence is now caller-supplied region → configured region → error. A blank string counts as "not provided" and falls through rather than erroring.

@@ -2364,10 +1233,9 @@ def check_recovery_service_limits(
"""
try:
request_id = uuid.uuid4().hex
config = _load_oci_config_for_server()
resolved_compartment_id = get_tenancy()
target_region = (config.get("region") or "us-ashburn-1").strip()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we raise an error if region cannot be derived and assume us-ashburn-1 .

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed together with the above - no default region is assumed anywhere now. If neither the argument nor the config yields one, the tool raises with a message naming both ways to supply it. (There was an existing test asserting the old behaviour by passing region="ignored"; updated.)

@@ -2430,37 +1299,44 @@ def _as_dict(obj: Any) -> dict[str, Any]:


@mcp.tool(
annotations=_READ_ONLY_TOOL,
description=(
"Lists the tenancy's subscribed regions and their status using "
"IdentityClient.list_region_subscriptions(). "
"NOTE: The 'service' parameter is accepted for backward compatibility but is "

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we review and rewrite the tool description ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten. It documented a service parameter that doesn't exist, and described tenancy_id as a compartment OCID.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is always empty

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed - has_backups_db_names was never appended to. Fixed, with a test.

@@ -3667,13 +2743,22 @@ def summarize_protected_database_backup_destination(
max_db_homes: Annotated[Optional[int], "Max number of DB Homes to scan."] = None,
max_total_databases: Annotated[Optional[int], "Global cap on databases to scan."] = None,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not honoured well, databases list can reach beyond this number.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The cap is now enforced inside the scan loop and the result sliced to it, so the list can't exceed max_total_databases.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this sorting by time ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't - it was comparing a mix of datetime objects and raw strings, so ordering was lexicographic and wrong whenever the two forms met on the same day. Added _as_instant(), which parses to a tz-aware datetime (assuming UTC when naive) before comparing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for doing de-dup in the end ? Will that not affect the other list entries?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that it affected the other lists - the count and the name lists were built from the un-deduplicated rows, so total_databases could exceed the number of entries actually reported. Moved the de-dup up to the scan (seen_database_ids), so every downstream list and count derives from the same de-duplicated set.

)
)
@_tool_logger("summarize_protected_database_backup_destination")
@telemetry._tool_logger("summarize_protected_database_backup_destination")
def summarize_protected_database_backup_destination(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a huge method with many nested methods and loops. Can we simplify this code ?

@arampuri arampuri Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified: 443 → 172 lines, 10 nested functions → 0, max loop depth 3 → 1. The shape-reading helpers are now module-level, and the body is three named phases - _scan_available_databases, _backup_destinations_for, _latest_backup_time.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add deadline check here as well?

@arampuri arampuri Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added. summarize_backup_space_used now takes the same deadline, and reports scanned_compartments / compartmentIdsInScope / truncated so a partial result is visible rather than silent. The destination summary sets truncated from the deadline as well.

arampuri and others added 2 commits September 9, 2026 17:17
Removed custom cache and included standard cache
Split server.py
And few other review comments
def _fetch_all_compartments(*, request_id: Optional[str] = None) -> list[Any]:
"""
Return all accessible ACTIVE compartments in the tenancy (plus root tenancy),
cached in-process so repeated Identity scans in one session cost one call.

@gebhardtr gebhardtr Sep 16, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if permissions are revoked during the session? we shouldn't cache anything that requires permission to access without a permission revalidation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cache never decides what a caller can access. It holds compartment metadata only, kept separately for each caller and expired after ORACLE_MCP_COMPARTMENT_CACHE_TTL_SECONDS. Every call that returns data (protected databases, backups, DB homes) is signed with the caller's own credentials, so a caller whose access was revoked gets nothing back. The most they can see during the TTL is compartment names and OCIDs they could already see a few minutes earlier. Here the listing only sets scope, and checking permissions again would mean the same full subtree scan the cache exists to avoid, on every call.

aggregated=aggregated,
per_compartment=per_compartment,
compartmentIdsScanned=scanned_compartments,
truncated=deadline.expired,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is missing on ProtectedDatabaseHealthCounts; can result in truncated=False even when the wrapping response contains True.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The aggregated counts now get partial=deadline.expired in both the health and redo summaries. The deadline test checks the aggregated flag as well as the per-compartment one.

continue
if sid in seen_database_ids:
continue
seen_database_ids.add(sid)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the database is added before _backup_destinations_for runs. If _backup_destinations_for fails, what will happen?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The ID is added to seen_database_ids only after _backup_destinations_for succeeds, so a database that fails to read stays out of every count, and a later duplicate of it gets retried.

Comment thread src/oci-recovery-mcp-server/README.md Outdated
`ORACLE_MCP_AUTH_PROFILE` remain supported, so existing configurations keep working,
but they are optional and no longer needed.

Configuration comes from environment variables, which may also be placed in a `.env`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this still work? I think it might have been broken in the refactor

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Akshat can you please check this ? Perhaps we are still not using .env ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corresponding doc has been updated now.

tool_id_token = _MCP_TOOL_ID_CONTEXT.set(tool_name)
logging_setup._log_event(
"tool_call",
request_id=request_id,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is generated per tool, so the tool and OCI events/opc-request-id aren't correlated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. _tool_logger now puts its request ID in a ContextVar. The tool body, the helpers and _make_client all read it instead of generating their own, so the tool_call events, the oci_call events and the opc-request-id all carry the same ID. A new test checks this end to end, including a client built without an explicit ID.



def _mcp_actor_id() -> str:
"""Return a privacy-safe opaque identifier for the active MCP user/session."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this statement: if sub is returned, than principal contains a value that uniquely identifies the user. How is that privacy protecting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a documentation issue, has been now resolved.
The ID goes only into opc-request-id, and every request carrying it is already signed with that caller's own credentials, so OCI learns nothing it doesn't already know. The hash keeps a raw sub (often a username or email) out of a value that gets copied into logs and support tickets, while distinct callers can still be counted.


# Two OCI calls per database on top of the compartment/home/page walk, so this
# is the heaviest fan-out of the four summaries and needs the same budget.
deadline = app._Deadline()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a ton of work that's happening before we start measuring against the deadline; could this cause discovery to go over the 120s budget on a cold cache?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we considering the deadline now along with compartment expansion ? Akshat could you please check ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. All four summary tools now start the deadline before compartment and DB home discovery, and the discovery loop checks it, so a large scope stops and returns truncated=True instead of using up the budget before it starts. A new test checks that the deadline starts before discovery in all four tools and that no DB home lookup happens once it has run out.

)
except Exception:
# Continue on per-DB errors to maximize overall coverage
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this handle spurious failures? retries? if a db has an intermittent early failure it will be skipped for how long?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A failure isn't stored anywhere, so a database is skipped only for the call that failed. The next call reads it again, and a duplicate later in the same scan is retried. The OCI client wrapper also logs each failure at ERROR, and with the request-ID fix that log entry now links to its tool call. On retries: get_database and list_backups don't retry by default in the SDK, so they now use a limited strategy covering 429, timeouts and transient 5xx, with at most 3 attempts and 10 seconds in total.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

OCA Verified All contributors have signed the Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants