Skip to content

release: v2.5.0#208

Merged
buildswithpaul merged 12 commits into
mainfrom
develop
Jun 15, 2026
Merged

release: v2.5.0#208
buildswithpaul merged 12 commits into
mainfrom
develop

Conversation

@buildswithpaul

Copy link
Copy Markdown
Owner

Release v2.5.0 — merges the current develop cycle into main. On push to main, semantic-release auto-bumps the version, tags, and publishes the GitHub Release.

Version bump: one feat: commit since v2.4.3 (#202) → MINOR → 2.5.0.

Change log: frappe_assistant_core/change_log/v2/v2_5_0.md (added in this cycle).

Included since v2.4.3

New Features

Security

Fixes

Improvements

  • OAuth URL resolution centralized in _get_public_base_url()

Post-merge

  • semantic-release publishes v2.5.0
  • Merge main back into develop

yassinejallouli and others added 12 commits May 19, 2026 17:12
- Use host_name from site config if available
- Force https:// when host_name is configured
- Fixes http -> https mismatch for production deployments
- Fixes #156
- Add _get_public_base_url() helper that respects host_name and force_https
- Replace get_server_url() at all 5 call sites in oauth_discovery.py
- Override Frappe-inherited URL keys in both openid_configuration() and
  authorization_server_metadata() to ensure issuer consistency
- Drop trailing slash on issuer per RFC 8414
- Fixes #156
get_doctype_info previously returned only top-level fields, so callers
hitting create_document for a parent doctype (Sales Order, Purchase
Order, etc.) had no way to discover the child DocType's own fields and
ended up creating the parent with empty child tables. create_document's
own description already instructs the model to call get_doctype_info
first, so the gap left the workflow broken.

Add a child_tables list to the response — one entry per Table /
Table MultiSelect field — including each child DocType's full field
metadata (same shape as the parent fields array). Callers now have
everything they need in a single tool call.

Also fix is_single, which was reading meta.istable (child-table flag)
instead of meta.issingle, and add a separate is_child_table key so the
two concepts stop being conflated.

Tests: regression guard asserting `User` exposes its `roles` child
table with recursive field metadata, plus a single-vs-child-table
distinguishing test against `System Settings` and `Has Role`.
The MCP endpoint used a single module-level MCPServer instance and rebuilt
its shared _tool_registry on every request (clear() + re-register). With
concurrent tools/call requests in the same worker, one request could clear
or overwrite the registry while another was validating or executing a tool
against it, causing intermittent "Tool 'X' not found. Available tools: []"
errors and only one of several concurrent executions being audited.

Build the tool registry per request on the call stack and thread it through
handle() / _handle_tools_list() / _handle_tools_call() instead of mutating
shared state:

- Add build_tool_dict() in tool_adapter to produce a tool dict without
  touching server/global state; register_base_tool() now reuses it.
- handle() and the tools/* handlers accept an optional tool_registry and
  route against it, falling back to self._tool_registry for direct add_tool
  usage. The register() wrapper passes through the dict returned by the
  endpoint.
- fac_endpoint builds a fresh per-user OrderedDict via _build_tool_registry()
  and returns it; no per-request mutation of the shared singleton.

Add test_mcp_concurrency regression tests: per-request registry isolation and
N overlapping threaded tools/call requests against one shared MCPServer. Tool
execution is kept in memory so the threaded test needs no DB connection and
stays within the test transaction. The tests fail against the old shared-
registry code with the exact reported "tool not found" symptom and pass with
the fix.

Fixes #197
The search tools query records with frappe.get_all, which bypasses DocType
and user/row-level permissions. Because the registered BaseTool search tools
(search_documents, search_doctype) delegate to SearchTools.global_search and
SearchTools.search_doctype, a restricted user could get back records they
cannot read — the same permission-leak class reported in #189 for
list_documents, via a different tool.

- global_search and search_doctype now use frappe.get_list(...,
  ignore_permissions=False) instead of frappe.get_all. The existing
  has_permission(doctype, "read") checks only gate DocType-level access and
  do not apply user permissions / row-level filtering; get_list does.
- search_link already uses frappe.desk.search.search_link (permission-aware)
  and is unchanged.

Also remove the dead legacy DocumentTools class (document_tools.py). It still
used frappe.get_all / frappe.db.count, but is not registered by any plugin,
referenced by hooks, or a BaseTool subclass, so it was unreachable — deleting
it prevents the leaky implementation from being wired back up later. The live
list_documents tool (DocumentList) was already fixed and is unaffected.

Add regression guards in test_search_tools that patch frappe.get_all to raise
and assert global_search / search_doctype query via frappe.get_list with
ignore_permissions=False. The tests fail against the old get_all code and pass
with the fix.

Fixes #189
…#196)

The MCP endpoint's 401 WWW-Authenticate header advertises the OAuth
resource_metadata URL using frappe.oauth.get_server_url(). That helper
reconstructs the host/port from the request URL or the Social Login Key
base_url and does not honor a configured host_name's non-standard port, so
the advertised URL dropped the port (e.g. :8000). Behind a port-restricted
network where the public URL is only reachable on a non-standard port, the
client then tries the implied :443 and the OAuth handshake fails with
"couldn't reach the MCP server".

The discovery endpoints already build URLs from _get_public_base_url(), which
uses host_name verbatim (scheme + port) — only the WWW-Authenticate header was
still on get_server_url().

- Expose get_public_base_url() in oauth_discovery as the shared accessor for
  the canonical public base URL.
- fac_endpoint now builds all six resource_metadata URLs (bearer-not-found,
  token-validation-error, API-auth-error, API-other-error, no-auth, and the
  HEAD probe) from get_public_base_url() instead of get_server_url(), so the
  configured port is preserved.

Add test_oauth_metadata_port: get_public_base_url returns a host_name with a
non-standard port verbatim (and does not consult get_server_url), and the MCP
401 WWW-Authenticate resource_metadata URL keeps the port on both the
unauthenticated and invalid-bearer paths. The header tests fail against the
old get_server_url code and pass with the fix.

Fixes #196
MCP clients (e.g. Claude Desktop) group tools and choose default approval
behavior from the annotation hints in the tools/list response. FAC emitted no
annotations, so every tool landed in a single "Other tools" bucket needing
approval, while servers that send hints (Google Drive, etc.) show proper
Read-only vs Write/delete groups.

Derive the hints from the existing FAC tool category — the same value shown
and overridable on the FAC admin page (FAC Tool Configuration.tool_category) —
so the client grouping stays in sync with the admin page (one source of truth):

    read_only  -> {readOnlyHint: true}                  (Read-only group)
    write      -> {readOnlyHint: false}                 (Write/delete group)
    read_write -> {readOnlyHint: false}                 (Write/delete group)
    privileged -> {readOnlyHint: false, destructiveHint: true}

- Add category_to_annotations() in tool_category_detector (co-located with the
  category logic; accepts the legacy "dangerous" alias; unknown -> no hints).
- _build_tool_registry resolves each tool's category once via
  _resolve_tool_categories (stored config/override -> auto-detect ->
  read_write, batch-fetched) and merges the derived hints onto each tool dict.
  The MCP server already forwards a tool dict's annotations in tools/list.

The transport already supported annotations; only the data source was empty.

Add test_tool_annotations: the category->hint mapping, category resolution
(stored override wins over auto-detect; fallbacks), tools/list emitting the
hints, and an end-to-end check that every built tool is classified.

Closes the "FAC tools show under Other tools" categorization gap.
FAC pinned beautifulsoup4~=4.12.2, which locks to the 4.12.* series. Frappe
v15 requires ~=4.12.2 but Frappe v16 requires ~=4.13.5, so installing FAC on a
v16 bench resolved bs4 down to 4.12.x — downgrading a dependency the rest of
the bench expected at 4.13.x.

Relax to beautifulsoup4>=4.12,<5 so the constraint spans both Frappe v15
(4.12.x) and v16 (4.13.x) without forcing a downgrade. FAC uses bs4 only as a
transitive convenience and does not depend on any 4.12-vs-4.13 behavior.

Fixes #198 (Observation 1). Observation 2 (optional analytics extras) is a
larger packaging change tracked separately.
…#203)

report_requirements returned empty filter definitions for custom Script
Reports whose filters live in the .js file, and failed silently — agents and
users had no way to see why. Reproduced two real triggers against a custom-app
Script Report:

- JSON-style quoted keys ("fieldname": "x"): the parser regex only matched
  bare keys (fieldname:), so every filter object was skipped -> 0 filters.
- Filters built programmatically (filters: get_filters()): there is no literal
  array to anchor on, so parsing produced nothing with no explanation.

Changes:
- Add Report.filters child table as the first (structured, no-regex) discovery
  source, then JS.
- Resolve the .js path via Frappe's own get_module_path + scrub (guarded by
  Module Def.custom) instead of reconstructing it by looping over installed
  apps, and fall back to the Report.javascript DB field for custom/DB-only
  modules with no on-disk file.
- Make the filter-object regexes tolerate quoted keys and template-literal
  labels.
- Surface discovery_diagnostics in the response: what each source attempted,
  the resolved path / file_exists / file_readable, filters_found, and a note
  when a source could not be parsed (e.g. programmatically-built filters), so
  an empty result is debuggable instead of silent.

Native reports (e.g. Sales Analytics, 8 filters) and existing report tests are
unaffected.

Add test_report_requirements_filters covering quoted/bare/template-literal/
programmatic JS, child-table conversion, and discovery orchestration. The
quoted-keys case fails against the old regex and passes with the fix.

Fixes #203
@buildswithpaul buildswithpaul merged commit 5e6020f into main Jun 15, 2026
13 checks passed
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.

4 participants