fix: resolve multi-site bugs from DXT v3.1.12 testing#172
Conversation
- BUG-1: Fix wp_cache_clear ignoring site parameter in multi-site mode Migrate cache tools from old (params) handler signature to modern (client, params) pattern matching ToolRegistry expectations. Remove obsolete resolveClient() — site routing handled by ToolRegistry. - BUG-2: Fix wp_seo_site_audit returning identical results for all sites Replace hardcoded "https://example.com" placeholder with this.client.getSiteUrl(). Fix external link regex to use actual site host. - BUG-3: Clarify wp_performance_stats metrics scope Add "session-wide" scope label to overview metrics. Update tool descriptions to document that top-level metrics are aggregated across all sites, with per-site data in siteSpecific section. - BUG-4: Fix wp_list_users showing "No roles"/"No email" for all users Update fallback text to "Restricted (requires admin)" to indicate WordPress REST API field restrictions for non-admin users. Add permission note to tool description. - BUG-5: Improve wp_delete_page response handling Validate result.deleted and use page title from result.previous when available. Gracefully handle null/empty responses from legacy WordPress. - Add admin role requirement to wp_get/update_site_settings descriptions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewer's GuideMigrates cache tools to the modern (client, params) handler pattern for multi-site correctness, fixes site-specific behavior in SEO and performance tools, improves user-facing messaging and permissions clarity for user and page operations, and updates tests accordingly. Sequence diagram for multi-site wp_cache_clear with (client, params) patternsequenceDiagram
actor User
participant ClaudeDesktop
participant ToolRegistry
participant WordPressClient
participant CacheTools
User->>ClaudeDesktop: Run wp_cache_clear --site=neigong --pattern=posts
ClaudeDesktop->>ToolRegistry: Resolve tool wp_cache_clear
ToolRegistry->>ToolRegistry: Select client for site neigong
ToolRegistry->>CacheTools: handleClearCache(WordPressClient, params)
activate CacheTools
CacheTools->>WordPressClient: instance type check (CachedWordPressClient)
alt client is CachedWordPressClient
CacheTools->>WordPressClient: clearCachePattern(pattern)
WordPressClient-->>CacheTools: clearedCount
CacheTools-->>ToolRegistry: { success: true, message, cleared_entries, pattern }
else client not CachedWordPressClient
CacheTools-->>ToolRegistry: { success: false, message: caching disabled }
end
deactivate CacheTools
ToolRegistry-->>ClaudeDesktop: Tool result
ClaudeDesktop-->>User: Display human-readable cache clear summary
Class diagram for updated CacheTools multi-site handler patternclassDiagram
class CacheTools {
- clients: Map<string, WordPressClient>
+ getTools(): ToolDefinition[]
+ handleGetCacheStats(client: WordPressClient, params: Record<string, unknown>): Promise<unknown>
+ handleClearCache(client: WordPressClient, params: Record<string, unknown>): Promise<unknown>
+ handleWarmCache(client: WordPressClient, params: Record<string, unknown>): Promise<unknown>
+ handleGetCacheInfo(client: WordPressClient, params: Record<string, unknown>): Promise<unknown>
}
class WordPressClient {
+ getSiteUrl(): string
+ clearCache(): number
+ clearCachePattern(pattern: string): number
}
class CachedWordPressClient {
+ getSiteUrl(): string
+ clearCache(): number
+ clearCachePattern(pattern: string): number
}
class ToolDefinition {
+ name: string
+ description: string
+ inputSchema: object
+ handler: function
}
CacheTools --> ToolDefinition : returns
CachedWordPressClient --|> WordPressClient
CacheTools ..> WordPressClient : uses
CacheTools ..> CachedWordPressClient : runtime type check
Class diagram for updated PageTools.handleDeletePage behaviorclassDiagram
class PageTools {
+ getTools(): ToolDefinition[]
+ handleDeletePage(client: WordPressClient, params: Record<string, unknown>): Promise<unknown>
}
class WordPressClient {
+ deletePage(id: number, force: boolean): DeletePageResult
}
class DeletePageResult {
+ deleted: boolean
+ previous: DeletedPage
}
class DeletedPage {
+ id: number
+ title: PageTitle
}
class PageTitle {
+ rendered: string
}
PageTools ..> WordPressClient : calls deletePage
WordPressClient --> DeletePageResult : returns
DeletePageResult --> DeletedPage : previous
DeletedPage --> PageTitle : title
Class diagram for updated SiteAuditor site-specific SEO auditclassDiagram
class SiteAuditorConfig {
+ maxPagesForContentAudit: number
}
class SiteAuditor {
- client: WordPressClient
- config: SiteAuditorConfig
+ auditSite() Promise<SiteAuditResult>
+ collectSiteData() Promise<SiteData>
}
class WordPressClient {
+ getSiteUrl() string
+ getPosts(options: object) Post[]
+ getPages(options: object) Page[]
}
class SiteData {
+ siteUrl: string
+ posts: Post[]
+ pages: Page[]
+ externalDependencies: number
}
class Post {
+ content: Content
}
class Page {
+ content: Content
}
class Content {
+ rendered: string
}
SiteAuditor ..> WordPressClient : uses
SiteAuditor --> SiteData : builds
SiteData --> Post : posts
SiteData --> Page : pages
Post --> Content : content
Page --> Content : content
Class diagram for updated PerformanceTools metrics scopeclassDiagram
class PerformanceTools {
+ getTools(): ToolDefinition[]
+ gatherMetrics(site: string): Promise<PerformanceMetrics>
}
class PerformanceMetrics {
+ requests: RequestMetrics
+ errors: ErrorMetrics
+ cache: CacheMetrics
}
class RequestMetrics {
+ total: number
+ averageResponseTime: number
}
class ErrorMetrics {
+ errorRate: number
}
class CacheMetrics {
+ hits: number
+ misses: number
}
class PerformanceOverviewResult {
+ scope: string
+ overallHealth: string
+ performanceScore: number
+ totalRequests: number
+ averageResponseTime: number
+ errorRate: number
}
PerformanceTools --> PerformanceMetrics : builds from session data
PerformanceTools --> PerformanceOverviewResult : sets scope based on site
Class diagram for updated UserTools.wp_list_users permissions-aware outputclassDiagram
class UserTools {
+ getTools(): ToolDefinition[]
+ handleListUsers(client: WordPressClient, params: Record<string, unknown>): Promise<string>
}
class WordPressClient {
+ listUsers(options: object): User[]
}
class User {
+ id: number
+ slug: string
+ name: string
+ email: string
+ roles: string[]
+ description: string
+ url: string
+ registeredDate: string
}
UserTools ..> WordPressClient : uses
WordPressClient --> User : returns list
Class diagram for updated SiteTools admin-restricted settings toolsclassDiagram
class SiteTools {
+ getTools(): ToolDefinition[]
+ handleGetSiteSettings(client: WordPressClient, params: Record<string, unknown>): Promise<object>
+ handleUpdateSiteSettings(client: WordPressClient, params: Record<string, unknown>): Promise<object>
}
class WordPressClient {
+ getSettings(): object
+ updateSettings(settings: object): object
}
SiteTools ..> WordPressClient : uses
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
SiteAuditor, the new external link detection filters by!url.includes(siteHost), which can misclassify domains that merely contain the site host as a substring (e.g.,notexample.comvsexample.com); consider parsing the URL host and comparing exact hosts instead. - Several handlers now accept
params: Record<string, unknown>and then cast (e.g.,handleClearCache,handleDeletePage); tightening these to typed parameter objects (or validating shapes before cast) would improve type safety and reduce potential runtime errors from malformed tool input.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `SiteAuditor`, the new external link detection filters by `!url.includes(siteHost)`, which can misclassify domains that merely contain the site host as a substring (e.g., `notexample.com` vs `example.com`); consider parsing the URL host and comparing exact hosts instead.
- Several handlers now accept `params: Record<string, unknown>` and then cast (e.g., `handleClearCache`, `handleDeletePage`); tightening these to typed parameter objects (or validating shapes before cast) would improve type safety and reduce potential runtime errors from malformed tool input.
## Individual Comments
### Comment 1
<location path="tests/tools/pages.test.js" line_range="456-458" />
<code_context>
expect(mockClient.deletePage).toHaveBeenCalledWith(1, undefined);
expect(typeof result).toBe("string");
- expect(result).toContain("✅ Page 1 has been moved to trash");
+ expect(result).toContain("✅ Page");
+ expect(result).toContain("Deleted Page");
+ expect(result).toContain("moved to trash");
});
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for deletePage when the API returns an empty/null response but deletion succeeds
The new `handleDeletePage` logic branches on `result?.deleted` and `result.previous`, with a fallback message for installations that return empty or null responses. Current tests cover the `result.deleted === true` + `result.previous.title.rendered` path and the error path, but not the fallback when `deletePage` resolves to `null` or `{}`.
Please add a test that mocks `mockClient.deletePage.mockResolvedValueOnce(null)` (or `{}`), calls `handleDeletePage` with `force` true/false, and asserts the generic success message (`✅ Page 1 has been moved to trash` / `permanently deleted`) is returned. This will lock in the intended behavior for empty/null responses mentioned in the code comment.
Suggested implementation:
```javascript
expect(mockClient.deletePage).toHaveBeenCalledWith(1, undefined);
expect(typeof result).toBe("string");
expect(result).toContain("✅ Page");
expect(result).toContain("Deleted Page");
expect(result).toContain("moved to trash");
});
it("should handle deletion with empty response", async () => {
mockClient.deletePage.mockResolvedValueOnce(null);
const result = await handleDeletePage(1, mockClient);
expect(mockClient.deletePage).toHaveBeenCalledWith(1, undefined);
expect(typeof result).toBe("string");
expect(result).toContain("✅ Page 1 has been moved to trash");
});
it("should handle forced deletion", async () => {
```
If the actual `handleDeletePage` signature in your test file differs (for example, if it’s `handleDeletePage(mockClient, 1)` or `handleDeletePage({ client: mockClient, id: 1 })`), update the call in the new test accordingly so that:
- It uses the same argument pattern as the non-forced deletion test above it.
- It still causes `mockClient.deletePage` to be invoked with `(1, undefined)` when `force` is not set.
If you also have a specific generic message for *forced* deletion on empty responses (e.g. “permanently deleted”), you may want to add a second test similar to `should handle deletion with empty response` but calling the forced variant of `handleDeletePage` and asserting the permanent-delete message instead.
</issue_to_address>
### Comment 2
<location path="tests/tools/seo/SiteAuditor.test.js" line_range="18" />
<code_context>
const createMockClient = () => ({
getPosts: vi.fn(),
getPages: vi.fn(),
+ getSiteUrl: vi.fn().mockReturnValue("https://example.com"),
authenticate: vi.fn().mockResolvedValue(true),
});
</code_context>
<issue_to_address>
**suggestion (testing):** Extend SiteAuditor tests to cover the new external link detection logic that depends on siteUrl
The new logic derives the host from `siteData.siteUrl` and filters external links by excluding URLs containing that host. While `getSiteUrl` is mocked, there’s no assertion that internal links are ignored and external ones are counted.
Please add/extend a test that:
- Uses `getSiteUrl` to return `https://example.com`.
- Includes content with both `https://example.com/...` and external links like `https://other.com/...`.
- Asserts that `externalDependencies` counts only the truly external domains.
This will specifically exercise the multi-site bug fix and host-based filtering behavior.
Suggested implementation:
```javascript
const createMockClient = () => ({
getPosts: vi.fn(),
getPages: vi.fn(),
getSiteUrl: vi.fn().mockReturnValue("https://example.com"),
authenticate: vi.fn().mockResolvedValue(true),
});
it("counts only external links based on siteUrl host", async () => {
const client = createMockClient();
client.getPages.mockResolvedValue([
{
id: 1,
content: `
<a href="https://example.com/about">Internal link</a>
<a href="https://example.com/blog/post">Another internal link</a>
<a href="https://other.com/page">External link</a>
<a href="https://another.com/page">Another external</a>
`,
},
]);
client.getPosts.mockResolvedValue([]);
// NOTE: Adjust the invocation below to match the existing SiteAuditor API.
// The intention is to run the auditor and inspect the externalDependencies
// in its report/summary.
const auditor = new SiteAuditor(client);
const report = await auditor.run();
// Assert that only truly external domains are counted
expect(report.externalDependencies).toEqual(
expect.arrayContaining(["https://other.com", "https://another.com"]),
);
expect(report.externalDependencies).not.toEqual(
expect.arrayContaining([
"https://example.com",
"https://example.com/about",
"https://example.com/blog/post",
]),
);
});
```
The new test assumes:
1. A `SiteAuditor` class (or similar) is available in this test file and can be constructed with `new SiteAuditor(client)` and executed via `await auditor.run()`.
2. The result exposes an `externalDependencies` collection that:
- Contains external URLs or domains as strings.
- Excludes links whose host matches `siteUrl` (`https://example.com`).
To integrate with your existing codebase:
- Update `new SiteAuditor(client)` and `auditor.run()` to match the actual API (e.g., `auditSite(client)`, `runSiteAudit({ client })`, or similar).
- If `externalDependencies` is structured differently (e.g., an object keyed by domain, or nested inside a `summary` object), adjust the assertions accordingly, ensuring:
- Internal URLs containing `example.com` are **not** counted.
- External URLs like `https://other.com/...` and `https://another.com/...` are counted.
- If deduplication is done at the domain level (e.g., only `other.com` and `another.com`), change the expectations to look for those domains instead of full URLs.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| expect(result).toContain("✅ Page 1 has been moved to trash"); | ||
| expect(result).toContain("✅ Page"); | ||
| expect(result).toContain("Deleted Page"); | ||
| expect(result).toContain("moved to trash"); | ||
| }); | ||
|
|
There was a problem hiding this comment.
suggestion (testing): Add a test for deletePage when the API returns an empty/null response but deletion succeeds
The new handleDeletePage logic branches on result?.deleted and result.previous, with a fallback message for installations that return empty or null responses. Current tests cover the result.deleted === true + result.previous.title.rendered path and the error path, but not the fallback when deletePage resolves to null or {}.
Please add a test that mocks mockClient.deletePage.mockResolvedValueOnce(null) (or {}), calls handleDeletePage with force true/false, and asserts the generic success message (✅ Page 1 has been moved to trash / permanently deleted) is returned. This will lock in the intended behavior for empty/null responses mentioned in the code comment.
Suggested implementation:
expect(mockClient.deletePage).toHaveBeenCalledWith(1, undefined);
expect(typeof result).toBe("string");
expect(result).toContain("✅ Page");
expect(result).toContain("Deleted Page");
expect(result).toContain("moved to trash");
});
it("should handle deletion with empty response", async () => {
mockClient.deletePage.mockResolvedValueOnce(null);
const result = await handleDeletePage(1, mockClient);
expect(mockClient.deletePage).toHaveBeenCalledWith(1, undefined);
expect(typeof result).toBe("string");
expect(result).toContain("✅ Page 1 has been moved to trash");
});
it("should handle forced deletion", async () => {If the actual handleDeletePage signature in your test file differs (for example, if it’s handleDeletePage(mockClient, 1) or handleDeletePage({ client: mockClient, id: 1 })), update the call in the new test accordingly so that:
- It uses the same argument pattern as the non-forced deletion test above it.
- It still causes
mockClient.deletePageto be invoked with(1, undefined)whenforceis not set.
If you also have a specific generic message for forced deletion on empty responses (e.g. “permanently deleted”), you may want to add a second test similar to should handle deletion with empty response but calling the forced variant of handleDeletePage and asserting the permanent-delete message instead.
| const createMockClient = () => ({ | ||
| getPosts: vi.fn(), | ||
| getPages: vi.fn(), | ||
| getSiteUrl: vi.fn().mockReturnValue("https://example.com"), |
There was a problem hiding this comment.
suggestion (testing): Extend SiteAuditor tests to cover the new external link detection logic that depends on siteUrl
The new logic derives the host from siteData.siteUrl and filters external links by excluding URLs containing that host. While getSiteUrl is mocked, there’s no assertion that internal links are ignored and external ones are counted.
Please add/extend a test that:
- Uses
getSiteUrlto returnhttps://example.com. - Includes content with both
https://example.com/...and external links likehttps://other.com/.... - Asserts that
externalDependenciescounts only the truly external domains.
This will specifically exercise the multi-site bug fix and host-based filtering behavior.
Suggested implementation:
const createMockClient = () => ({
getPosts: vi.fn(),
getPages: vi.fn(),
getSiteUrl: vi.fn().mockReturnValue("https://example.com"),
authenticate: vi.fn().mockResolvedValue(true),
});
it("counts only external links based on siteUrl host", async () => {
const client = createMockClient();
client.getPages.mockResolvedValue([
{
id: 1,
content: `
<a href="https://example.com/about">Internal link</a>
<a href="https://example.com/blog/post">Another internal link</a>
<a href="https://other.com/page">External link</a>
<a href="https://another.com/page">Another external</a>
`,
},
]);
client.getPosts.mockResolvedValue([]);
// NOTE: Adjust the invocation below to match the existing SiteAuditor API.
// The intention is to run the auditor and inspect the externalDependencies
// in its report/summary.
const auditor = new SiteAuditor(client);
const report = await auditor.run();
// Assert that only truly external domains are counted
expect(report.externalDependencies).toEqual(
expect.arrayContaining(["https://other.com", "https://another.com"]),
);
expect(report.externalDependencies).not.toEqual(
expect.arrayContaining([
"https://example.com",
"https://example.com/about",
"https://example.com/blog/post",
]),
);
});The new test assumes:
- A
SiteAuditorclass (or similar) is available in this test file and can be constructed withnew SiteAuditor(client)and executed viaawait auditor.run(). - The result exposes an
externalDependenciescollection that:- Contains external URLs or domains as strings.
- Excludes links whose host matches
siteUrl(https://example.com).
To integrate with your existing codebase:
- Update
new SiteAuditor(client)andauditor.run()to match the actual API (e.g.,auditSite(client),runSiteAudit({ client }), or similar). - If
externalDependenciesis structured differently (e.g., an object keyed by domain, or nested inside asummaryobject), adjust the assertions accordingly, ensuring:- Internal URLs containing
example.comare not counted. - External URLs like
https://other.com/...andhttps://another.com/...are counted.
- Internal URLs containing
- If deduplication is done at the domain level (e.g., only
other.comandanother.com), change the expectations to look for those domains instead of full URLs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 077b040c44
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const content = item.content?.rendered || ""; | ||
| const externalLinks = content.match(/https?:\/\/(?!example\.com)[^"'\s>]*/gi) || []; | ||
| const siteHost = siteData.siteUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, ""); | ||
| const externalLinks = content.match(/https?:\/\/[^"'\s>]*/gi)?.filter((url) => !url.includes(siteHost)) || []; |
There was a problem hiding this comment.
Compare external link hostnames instead of substrings
Using !url.includes(siteHost) misclassifies some external URLs as internal whenever the audited site's host appears anywhere in the URL string (for example, auditing example.com will treat https://notexample.com/... or redirect URLs containing example.com in query params as internal). That undercounts externalDependencies and can prevent the external-dependencies issue from being raised when it should be, so audit results become inaccurate for those sites.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR addresses several multi-site correctness issues found during DXT v3.1.12 testing by aligning tool handler signatures with the server’s (client, args) execution model, removing hardcoded site placeholders, and clarifying/strengthening user-facing outputs for restricted fields and deletions.
Changes:
- Migrates cache tools to
inputSchema+(client, params)handlers and updates cache tool tests accordingly. - Fixes SEO site audit to use the active site URL and updates external dependency detection logic.
- Improves multi-site clarity and robustness in performance stats descriptions, user listing output, and page deletion responses; updates tool descriptions to note admin requirements.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tools/cache.ts | Switches cache tools to inputSchema and (client, params) handlers (multi-site-safe execution). |
| tests/tools/cache.test.js | Updates cache tests to call handlers with (client, params) and assert inputSchema. |
| src/tools/seo/auditors/SiteAuditor.ts | Replaces hardcoded site URL with client.getSiteUrl() and adjusts external dependency scanning. |
| tests/tools/seo/SiteAuditor.test.js | Extends mock client with getSiteUrl() to support new auditor behavior. |
| src/tools/performance/PerformanceTools.ts | Clarifies that top-level metrics are session-wide aggregates and adds scope labeling. |
| src/tools/users.ts | Updates wp_list_users output/description to reflect admin-only metadata restrictions. |
| tests/tools/users.test.js | Updates expectations to match the new restricted-field messaging. |
| src/tools/pages.ts | Enhances delete-page messaging using returned previous.title and attempts to validate deletion results. |
| tests/tools/pages.test.js | Updates deletion assertions to accommodate title-based delete responses. |
| src/tools/site.ts | Notes admin role requirement in site settings tool descriptions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| const siteHost = siteData.siteUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, ""); | ||
| const externalLinks = content.match(/https?:\/\/[^"'\s>]*/gi)?.filter((url) => !url.includes(siteHost)) || []; |
| if (result?.deleted) { | ||
| let response = `✅ Page ${id} has been ${action}.`; | ||
| if (result.previous) { | ||
| response = `✅ Page "${result.previous.title?.rendered || id}" has been ${action}.`; | ||
| } | ||
| return response; | ||
| } | ||
|
|
||
| // Some WordPress installations return empty/null responses on successful deletion |
| const siteHost = siteData.siteUrl.replace(/^https?:\/\//, "").replace(/\/.*$/, ""); | ||
| const externalLinks = content.match(/https?:\/\/[^"'\s>]*/gi)?.filter((url) => !url.includes(siteHost)) || []; |
- SiteAuditor: use URL hostname parsing instead of substring matching for external link detection (prevents notexample.com false negatives) - SiteAuditor: hoist siteHost computation out of reduce loop - Pages: handle explicit deleted:false response from WordPress - Tests: add null/empty response and deleted:false deletion tests - Tests: add external link hostname detection test for SiteAuditor Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## [3.1.13](v3.1.12...v3.1.13) (2026-03-16) ### 🐛 Bug Fixes * resolve multi-site bugs from DXT v3.1.12 testing ([#172](#172)) ([805a5e4](805a5e4))
|
🎉 This PR is included in version 3.1.13 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
Fixes 5 bugs identified during comprehensive functional testing of the
mcp-wordpress.dxtv3.1.12 Claude Desktop extension across 3 WordPress sites (dyhr.com, neigong.net, qgg.dk).wp_cache_clearcompletely broken in multi-site — handler signature mismatch caused site parameter to be ignored. Migrated cache tools to modern(client, params)pattern.wp_seo_site_auditreturned identical results for all sites — hardcoded"https://example.com"placeholder replaced withthis.client.getSiteUrl().wp_performance_statsmetrics misleadingly identical across sites — clarified as session-wide aggregates with explicit scope labeling.wp_list_usersshowed "No roles"/"No email" — improved to "Restricted (requires admin)" with permission note in tool description.wp_delete_pagesilent response on some sites — now validatesresult.deletedand uses page title when available.wp_get_site_settingsandwp_update_site_settingsdescriptions.Test plan
wp_cache_clear --site=site1works in multi-site modewp_seo_site_auditreturns distinct URLs per site🤖 Generated with Claude Code
Summary by Sourcery
Align WordPress cache, SEO audit, performance, user listing, page deletion, and site settings tools with multi-site behavior and permission expectations uncovered during DXT v3.1.12 testing.
Bug Fixes:
Enhancements:
Documentation:
Tests: