Skip to content

[4/5] Expose the scoped announcement MCP runtime - #272

Draft
rebova-microsoft wants to merge 1 commit into
users/rebova/org-announcements-review-audiencefrom
users/rebova/org-announcements-review-runtime
Draft

rebova-microsoft wants to merge 1 commit into
users/rebova/org-announcements-review-audiencefrom
users/rebova/org-announcements-review-runtime

Conversation

@rebova-microsoft

Copy link
Copy Markdown
Contributor

Description

Expose the separate announcements MCP provider, widget resource, manager/editor opener, audience lookup, and widget-owned authoring operations.

Current diff

  • Keep flat opener arguments: manager requires titleId; create requires explicit create intent without an existing bulletinId; edit requires bulletinId. Reject malformed combinations before generating widget retry state.
  • Use ordinary editing instead of a dedicated republish opener mode. Preserve schedules and normal publish validation.
  • Preserve captured tenant/account directory leases, scoped retry context, and non-repeating recovery for indeterminate or committed-but-unrefreshed writes.
  • Add cross-provider discovery/import-isolation coverage and retain existing feature telemetry. No additional announcements client-event bridge is introduced.

Feature contract

The opener and discovery tools are read-only. The widget owns mutations after opening. Unknown ownership and malformed retry requests are not accepted. Backend publishing capability and existing Duplicate/Unarchive write timing are unchanged.

Stack and dependency

This is slice 4/5. Head: users/rebova/org-announcements-review-runtime. Base: users/rebova/org-announcements-review-audience. The current diff is only this slice against its immediate predecessor; these are stacked review chunks, not parallel PRs against the integration branch.

Bootstrap prerequisite: #262 merged into release/planner-landing-page on September 10, 2026. This stack remains pinned to cacb1bec056428809f1ccb0383561190d516bee4, which is an ancestor of merge commit 8a04f5f40f334e729a3497877edca655730f1be2. Unchanged prerequisite work is excluded from this slice. No rewrite is needed solely to account for that merge. The future release target remains TBD and its final promotion baseline must be confirmed separately.

Testing

612 Python tests passed from the clean committed slice, including protocol/recovery, privacy, cross-provider discovery, import isolation, and existing landing-page tests. Ruff and syntax checks passed.

Local execution used Python 3.13.15. Python 3.11 GitHub Actions validation is pending; no local 3.11 pass is claimed.

Readiness

  • Live Graph, authoring-service, hosted-widget, and end-to-end integration checks are deferred and remain required before rollout.
  • This is not release approval and performs no deployment. The future release branch is still TBD.
  • The prerelease integration branch remains baseline-only until reviewed chunks are merged through the normal review process.
  • Relevant offline tests and static checks pass.
  • Python 3.11 CI results.
  • Authorized live integration and end-to-end validation.

Expose separate announcement manager and editor entry points with validated flat arguments, scoped retry context, captured directory leases, and widget-owned mutations. Preserve indeterminate and committed-refresh recovery, safe telemetry, and independent provider discovery.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eddd3818-bb74-42d3-bcf3-7e0670a57f27

@rebova-microsoft rebova-microsoft left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Walkthrough — Runtime file guide.

This informational walkthrough maps all eight changed files. Runtime here connects agent/widget requests to the authoring and directory clients from PRs #270#271; it is not UI layout or backend storage. Toolkit startup and entry-point routing belong to PR #273.

Comment thread .github/workflows/ci.yml
tests/mcp/agentconfig_org_announcements/test_authoring_client.py
tests/mcp/agentconfig_org_announcements/test_drafts.py
tests/mcp/agentconfig_org_announcements/test_graph_directory.py
tests/mcp/agentconfig_org_announcements

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Walkthrough — Include the runtime checks in CI.

The announcements job now selects the whole feature test directory instead of naming three individual files. That includes the new protocol, client-lifecycle, and telemetry tests alongside the earlier client, draft, and directory tests, without adding a workflow entry for each file. The foundation job also gains test_import_isolation.py, which checks that the sibling MCP providers load their own modules. These changes expand test selection; they do not start or deploy the announcements service.

meta=_widget_tool_meta(),
annotations=_READ_ONLY_ANNOTATIONS,
)
async def open_org_announcements(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Walkthrough — Connect requests to announcement services.

This file connects requests to PR #270's authoring client and PR #271's Graph client—not screen layout or storage.

  • org_announcements_widget returns a static HTML shell loading Vorpal. Metadata exposes read tools to the model/app and mutations only to the app.
  • open_org_announcements takes flat titleId/view arguments. Manager has no mode; editor requires create or edit, with an ID only for edit. Pydantic checks combinations before widget-error handling: malformed calls become ToolError; valid failed opens retain the original request without inventing an ID/mode. Opening never saves.
  • Each operation captures its authoring client and authenticated tenant alongside titleId; directory work also binds the account. get_graph_client counts active users so replacing a tenant/account client does not close it mid-operation.
  • save_bulletin sends complete content/audience. transition_bulletin delegates identifier/status-only changes; publishing uses a full save, not a republish mode. duplicate_bulletin strips identity/audit fields and creates a Draft before any later editor opening, preserving its title and leaving the source unchanged.
  • _saved_item_result combines acknowledged content with refreshed manager/audience data. IndeterminateWrite and CommittedRefreshFailed are non-retryable results: inspect current state instead of blindly repeating a create/copy. A manager refresh does not guarantee identifying the saved record.

return _ADK_TELEMETRY


def record_operation(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Walkthrough — Measure operations without recording authored content.

record_operation adapts the runtime's calls to the existing adk.api.call emitter; it is not a separate widget-event bridge. The api_endpoint field holds an allowlisted tool name, not the service URL. Success adds outcome and duration; failure also adds a short identifier-shaped error code and a broad backend/Graph/MCP category, with an empty error message. Unknown operation/source values are replaced, and message-shaped or overlong codes become UnknownError. Error codes are shape-checked, not a fixed allowlist. The helper resolves the emitter lazily and tolerates its absence or emission exceptions, keeping those telemetry failures separate from the tool result.



@pytest.mark.parametrize("modules", [LANDING])
@pytest.mark.parametrize("modules", [LANDING, ANNOUNCEMENTS])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Walkthrough — Share discovery without coupling the feature providers.

Adding announcements to the existing parameterized tests applies the same discovery contract to both clients: shared helper methods, preserved titleId values, expected request routes, rejected invalid searches, and errors for malformed collections rather than empty successes. The feature-tool check uses a discovery-only fake, so listing/searching agents does not require configuration initialization or attach a widget. Announcements awaits its client and advertises readOnlyHint; the landing-page provider keeps its existing synchronous accessor and unchanged metadata. Search uses a POST request here, but its purpose is still to find existing agents, not create configuration.

assert org_server._client is None


def test_the_authoring_client_is_constructed_off_the_event_loop(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Walkthrough — Keep sign-in from blocking the request transport.

These tests cover how the server obtains and replaces its authoring client, rather than announcement content. Fakes check that construction happens off the async event-loop thread, concurrent first requests share one construction, and reset drops and closes the old client so a subsequent call can build another. Authoring-side 401 failures trigger that reset; Graph authentication failures and other tested service statuses do not. Mocked sign-in paths also check that notices go to stderr, not stdout, because stdout carries MCP protocol messages. Additional guards look for stdout prints and overly verbose HTTP logging. No real browser sign-in is needed for these scenarios.

return install


def _call(tool: str, arguments: dict[str, Any], *, include_scope: bool = True) -> Any:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Walkthrough — Exercise the contract the host and widget call.

_call goes through FastMCP's tool dispatcher, while fake authoring/directory clients record requests and inject failures. This covers resource metadata, tool visibility, structured results, complete saves, minimal transitions, duplicate creation, and audience lookup. The schema assertions intentionally require only titleId and view globally; separate invocation tests check the conditional manager/create/edit rules and ToolError boundary. Valid failed opens retain retry context instead. Other cases distinguish uncertain writes from acknowledged writes whose refresh fails, checking that the mutation is not repeated. Scope and overlapping-request cases check captured tenant/agent/account context and Graph-client replacement. These are host/runtime contract checks with simulated dependencies, not evidence about rendered UI or deployed backend behavior.



@pytest.fixture
def emitted(monkeypatch) -> list[dict[str, Any]]:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Walkthrough — Inspect what reaches the telemetry emitter.

The emitted fixture replaces the ADK emitter and records its fields, so assertions inspect the outgoing telemetry payload rather than assuming a caller omitted sensitive data. Tests cover exact success/failure field shapes, normalization of unexpected values, and missing or failing emitters. Mocked tool calls carry announcement text, audience IDs, and tenant/agent/account context; assertions check that the fixture values, suggested drafts, search text, and echoed backend messages stay out of the tested events. A final guard limits field names across the five instrumented operations. This targets the telemetry boundary, not a blanket ban on content in legitimate tool responses.

)


def test_both_servers_load_with_their_own_client_module() -> None:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Walkthrough — Prevent sibling servers from importing each other's code.

Both MCP providers have files named client.py and server.py. Loading them under those plain module names in one Python process can make the second provider reuse the first provider's module. These tests check the shared isolated loader returns distinct clients from the correct directories, reuses each provider's own modules on repeated loads, and restores previous plain-name entries. A source scan discourages direct flat imports in test modules; a separate subprocess checks that both providers coexist in one interpreter. This protects the test harness from misleading cross-provider failures, not tenant isolation or live service routing.

@rebova-microsoft
rebova-microsoft added this pull request to stack #282 September 14, 2026 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants