From 60c37a102eee10d267f8ae1777b0ace7623b1986 Mon Sep 17 00:00:00 2001
From: Adam Rauch
Date: Tue, 18 Aug 2026 15:27:22 -0700
Subject: [PATCH 1/2] Update TableSelectorTestCase to test more MySQL/MariaDB
databases (#7946)
## Rationale
Improve testing of MySQL and MariaDB databases
## Changes
- Detect MySQL and MariaDB data sources based on product name, not data
source name
- If `sakila` is not present, try testing with `sys.sys_config`
---
.../api/data/TableSelectorTestCase.java | 27 ++++++++++++++++---
.../api/data/dialect/StandardJdbcHelper.java | 2 +-
2 files changed, 25 insertions(+), 4 deletions(-)
diff --git a/api/src/org/labkey/api/data/TableSelectorTestCase.java b/api/src/org/labkey/api/data/TableSelectorTestCase.java
index 281681ebc2e..68d5c2ce623 100644
--- a/api/src/org/labkey/api/data/TableSelectorTestCase.java
+++ b/api/src/org/labkey/api/data/TableSelectorTestCase.java
@@ -17,6 +17,7 @@
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Logger;
import org.junit.Test;
import org.labkey.api.collections.CsvSet;
import org.labkey.api.data.Selector.ForEachBlock;
@@ -30,6 +31,7 @@
import org.labkey.api.util.ExceptionUtil;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.util.TestContext;
+import org.labkey.api.util.logging.LogHelper;
import org.springframework.jdbc.UncategorizedSQLException;
import java.sql.ResultSet;
@@ -50,6 +52,8 @@
public class TableSelectorTestCase extends AbstractSelectorTestCase
{
+ private static final Logger LOG = LogHelper.getLogger(TableSelectorTestCase.class, "Test progress");
+
@Test
public void testTableSelector() throws SQLException
{
@@ -59,13 +63,25 @@ public void testTableSelector() throws SQLException
// testTableSelector(DbSchema.get("oracle.granite", DbSchemaType.Bare).getTable("account"), Account.class);
// Test MySQL or MariaDB database, if present
- List mySqlScopes = Stream.of("mySql", "mariadb")
- .map(DbScope::getDbScope).filter(Objects::nonNull).toList();
+ List mySqlScopes = DbScope.getDbScopesToTest().stream()
+ .filter(scope -> Set.of("MySQL", "MariaDB").contains(scope.getSqlDialect().getProductName()))
+ .toList();
+
for (DbScope mySqlScope: mySqlScopes)
{
DbSchema sakila = mySqlScope.getSchema("sakila", DbSchemaType.Bare);
if (sakila.existsInDatabase())
- testTableSelector(sakila.getTable("country"), Country.class);
+ {
+ testTableSelector(sakila.getTable("Country"), Country.class);
+ }
+ else
+ {
+ DbSchema sys = mySqlScope.getSchema("sys", DbSchemaType.Bare);
+ if (sys.existsInDatabase())
+ {
+ testTableSelector(sys.getTable("sys_config"), Config.class);
+ }
+ }
}
testTableSelector(CoreSchema.getInstance().getTableInfoActiveUsers(), User.class);
testTableSelector(CoreSchema.getInstance().getTableInfoModules(), ModuleContext.class);
@@ -124,6 +140,8 @@ public int hashCode()
}
}
+ record Config(String Variable, String Value, Date Set_Time, String Set_By){}
+
// public static class Account
// {
// private int _account_id;
@@ -430,6 +448,9 @@ private void testColumnList(TableSelector selector, boolean stable) throws SQLEx
private void testTableSelector(TableInfo table, Class clazz) throws SQLException
{
+ DbSchema schema = table.getSchema();
+ LOG.info("Testing {}.{}.{}", schema.getScope().getDisplayName(), schema.getName(), table.getName());
+
TableSelector selector = new TableSelector(table);
test(selector, clazz);
diff --git a/api/src/org/labkey/api/data/dialect/StandardJdbcHelper.java b/api/src/org/labkey/api/data/dialect/StandardJdbcHelper.java
index 986b0e0fa97..9362ddcde91 100644
--- a/api/src/org/labkey/api/data/dialect/StandardJdbcHelper.java
+++ b/api/src/org/labkey/api/data/dialect/StandardJdbcHelper.java
@@ -44,7 +44,7 @@ protected String parseDatabase(String url) throws ServletException
if (-1 == dbEnd)
dbEnd = url.length();
- // Last '/' is the database delimiter, except for "jdbc:postgresql:database"
+ // Last '/' is the database delimiter, except for "jdbc:postgresql:database" and old Oracle formats
char dbDelimiter = url.contains("/") ? '/' : ':';
int dbDelimiterIndex = url.lastIndexOf(dbDelimiter, dbEnd);
From ae69984e01484f2bc6215321818a4637125c2e04 Mon Sep 17 00:00:00 2001
From: labkey-matthewb
Date: Wed, 19 Aug 2026 13:51:25 -0700
Subject: [PATCH 2/2] Proxy local calls to searchDocumentation() and
retrieveDocument() to remote www.labkey.org/mcp
---
.../org/labkey/api/action/ConfirmAction.java | 6 +-
api/src/org/labkey/api/admin/AdminUrls.java | 1 +
.../labkey/api/mcp/DocumentationService.java | 51 ++++++++
api/src/org/labkey/api/mcp/McpToolProxy.java | 114 ++++++++++++++++++
api/src/org/labkey/api/settings/AppProps.java | 5 +
api/src/org/labkey/api/wiki/WikiService.java | 5 +-
core/src/org/labkey/core/CoreMcp.java | 97 +++++++++++++++
.../org/labkey/core/DataAnalysis_Python.md | 4 +-
core/src/org/labkey/core/DataAnalysis_R.md | 4 +-
core/src/org/labkey/core/FileBasedModules.md | 6 +-
core/src/org/labkey/core/Reports.md | 2 +
.../labkey/core/admin/AdminController.java | 6 +
.../org/labkey/core/webdav/DavController.java | 6 +-
.../query/controllers/prompts/LabKeySql.md | 2 +
14 files changed, 293 insertions(+), 16 deletions(-)
create mode 100644 api/src/org/labkey/api/mcp/DocumentationService.java
create mode 100644 api/src/org/labkey/api/mcp/McpToolProxy.java
diff --git a/api/src/org/labkey/api/action/ConfirmAction.java b/api/src/org/labkey/api/action/ConfirmAction.java
index 32dd2efa002..16881e1d065 100644
--- a/api/src/org/labkey/api/action/ConfirmAction.java
+++ b/api/src/org/labkey/api/action/ConfirmAction.java
@@ -77,7 +77,10 @@ public final ModelAndView handleRequest() throws Exception
ModelAndView mv = getSuccessView(form);
if (null != mv)
return mv;
- throw new RedirectException(getSuccessURL(form));
+ URLHelper redirect = getSuccessURL(form);
+ if (null != redirect)
+ throw new RedirectException(redirect);
+ return null;
}
}
else
@@ -137,7 +140,6 @@ public void validate(@NotNull Object form, @NotNull Errors errors)
/* Generic version of validate */
public abstract void validateCommand(FORM form, Errors errors);
- @NotNull
public abstract URLHelper getSuccessURL(FORM form);
// not usually used but some actions return views that close the current window etc...
diff --git a/api/src/org/labkey/api/admin/AdminUrls.java b/api/src/org/labkey/api/admin/AdminUrls.java
index e3a9dd0f92f..8101dc012bd 100644
--- a/api/src/org/labkey/api/admin/AdminUrls.java
+++ b/api/src/org/labkey/api/admin/AdminUrls.java
@@ -69,6 +69,7 @@ public interface AdminUrls extends UrlProvider
ActionURL getAllowedExternalRedirectHostsURL();
ActionURL getDeleteEncryptedContentURL();
+ ActionURL getOptionalFeaturesURL();
/**
* Simply adds an "Admin Console" link to nav trail if invoked in the root container. Otherwise, root is unchanged.
diff --git a/api/src/org/labkey/api/mcp/DocumentationService.java b/api/src/org/labkey/api/mcp/DocumentationService.java
new file mode 100644
index 00000000000..af8b3e4e927
--- /dev/null
+++ b/api/src/org/labkey/api/mcp/DocumentationService.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.labkey.api.mcp;
+
+import org.jetbrains.annotations.Nullable;
+import org.labkey.api.services.ServiceRegistry;
+
+/**
+ * Local implementation of documentation search/retrieval, backing the {@code searchDocumentation}/
+ * {@code retrieveDocument} MCP tools in {@code CoreMcp}. Only registered on servers that host the LabKey
+ * documentation content and its vector store (currently www.labkey.org); every other server forwards those
+ * tool calls to www.labkey.org instead of calling this service. See {@link #isEnabled()}.
+ */
+public interface DocumentationService
+{
+ static @Nullable DocumentationService get()
+ {
+ return ServiceRegistry.get().getService(DocumentationService.class);
+ }
+
+ static void setInstance(DocumentationService impl)
+ {
+ ServiceRegistry.get().registerService(DocumentationService.class, impl);
+ }
+
+ /**
+ * True if this server is enabled as the documentation source. The backing optional feature flag is owned by
+ * whichever module registers an implementation (currently serviceTools), not this interface, so the flag
+ * only exists at all on servers that have that module installed.
+ */
+ boolean isEnabled();
+
+ /** Returns a JSON string; see CoreMcp's searchDocumentation tool description for the response shape. */
+ String searchDocumentation(String query, @Nullable Integer topK);
+
+ /** Returns a JSON string; see CoreMcp's retrieveDocument tool description for the response shape. */
+ String retrieveDocument(String id);
+}
\ No newline at end of file
diff --git a/api/src/org/labkey/api/mcp/McpToolProxy.java b/api/src/org/labkey/api/mcp/McpToolProxy.java
new file mode 100644
index 00000000000..0a49ccfc9bb
--- /dev/null
+++ b/api/src/org/labkey/api/mcp/McpToolProxy.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.labkey.api.mcp;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.logging.log4j.Logger;
+import org.labkey.api.util.logging.LogHelper;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Proxy for calling an MCP tool on a remote server.
+ *
+ * A single {@link McpSyncClient} is created lazily and reused for the lifetime of this instance; the MCP
+ * initialize handshake only happens once, on the first forwarded call.
+ */
+public class McpToolProxy
+{
+ private static final Logger LOG = LogHelper.getLogger(McpToolProxy.class, "MCP tool forwarding");
+
+ private final String remoteBaseUrl;
+ private volatile McpSyncClient client;
+
+ public McpToolProxy(String remoteBaseUrl)
+ {
+ this.remoteBaseUrl = remoteBaseUrl;
+ }
+
+ private McpSyncClient getClient()
+ {
+ McpSyncClient c = client;
+ if (c == null)
+ {
+ synchronized (this)
+ {
+ c = client;
+ if (c == null)
+ {
+ var transport = HttpClientStreamableHttpTransport.builder(remoteBaseUrl).build();
+ c = McpClient.sync(transport)
+ .clientInfo(McpSchema.Implementation.builder("labkey-server-forwarder", "1.0").build())
+ .build();
+ c.initialize();
+ client = c;
+ }
+ }
+ }
+ return c;
+ }
+
+ // Drop the cached client after a failed call, so the next attempt reconnects instead of reusing a dead session
+ private synchronized void resetClient()
+ {
+ if (client != null)
+ {
+ try
+ {
+ client.closeGracefully();
+ }
+ catch (RuntimeException ignore)
+ {
+ // already broken; nothing to do
+ }
+ client = null;
+ }
+ }
+
+ /**
+ * Calls {@code remoteToolName} on the remote MCP server with {@code arguments} and returns its text content
+ * (joined, if the tool returned more than one text content block). Throws if the remote server can't be
+ * reached or the remote tool itself reports an error.
+ */
+ public String forward(String remoteToolName, Map arguments)
+ {
+ McpSchema.CallToolResult result;
+ try
+ {
+ result = getClient().callTool(McpSchema.CallToolRequest.builder(remoteToolName).arguments(arguments).build());
+ }
+ catch (RuntimeException e)
+ {
+ LOG.error("Failed to forward MCP tool call '{}' to {}", remoteToolName, remoteBaseUrl, e);
+ resetClient();
+ throw new McpException("Unable to reach " + remoteBaseUrl + " to forward '" + remoteToolName + "': " + e.getMessage());
+ }
+
+ String text = result.content().stream()
+ .filter(content -> content instanceof McpSchema.TextContent)
+ .map(content -> ((McpSchema.TextContent) content).text())
+ .collect(Collectors.joining("\n"));
+
+ if (Boolean.TRUE.equals(result.isError()))
+ throw new McpException("Remote tool '" + remoteToolName + "' at " + remoteBaseUrl + " reported an error: " + text);
+
+ return text;
+ }
+}
\ No newline at end of file
diff --git a/api/src/org/labkey/api/settings/AppProps.java b/api/src/org/labkey/api/settings/AppProps.java
index 27480cc7b45..ddd18e11111 100644
--- a/api/src/org/labkey/api/settings/AppProps.java
+++ b/api/src/org/labkey/api/settings/AppProps.java
@@ -260,4 +260,9 @@ static WriteableAppProps getWriteableInstance()
@NotNull List getAllowedExtensions();
@NotNull String getAllowedExternalResourceHosts();
+
+ default @Nullable String getDocumentationServer()
+ {
+ return "https://www.labkey.org";
+ }
}
diff --git a/api/src/org/labkey/api/wiki/WikiService.java b/api/src/org/labkey/api/wiki/WikiService.java
index 3dfb4efadc1..4459e3f2655 100644
--- a/api/src/org/labkey/api/wiki/WikiService.java
+++ b/api/src/org/labkey/api/wiki/WikiService.java
@@ -124,9 +124,8 @@ default HtmlString getHtml(Container c, String name)
* Each {@link org.labkey.api.mcp.McpService.VectorDocument} is assigned an ID of the form
* {@code "/"}, where both components are GUIDs
* (as returned by {@link Container#getId()} and the wiki's own entity ID).
- * Tools that consume vector store results (e.g. {@code listDocuments},
- * {@code retrieveDocument}) must use this same format when constructing or
- * interpreting document IDs.
+ * Tools that consume vector store results (e.g. {@code retrieveDocument}) must use this same
+ * format when constructing or interpreting document IDs.
*
* @return the number of documents added
*/
diff --git a/core/src/org/labkey/core/CoreMcp.java b/core/src/org/labkey/core/CoreMcp.java
index cfd39dc0292..84734b1c225 100644
--- a/core/src/org/labkey/core/CoreMcp.java
+++ b/core/src/org/labkey/core/CoreMcp.java
@@ -24,6 +24,9 @@
import org.labkey.api.collections.LabKeyCollectors;
import org.labkey.api.data.Container;
import org.labkey.api.data.ContainerManager;
+import org.labkey.api.mcp.DocumentationService;
+import org.labkey.api.mcp.McpException;
+import org.labkey.api.mcp.McpToolProxy;
import org.labkey.api.mcp.McpService;
import org.labkey.api.module.ModuleLoader;
import org.labkey.api.security.RequiresNoPermission;
@@ -35,6 +38,7 @@
import org.labkey.api.study.Study;
import org.labkey.api.study.StudyService;
import org.labkey.api.util.HtmlString;
+import org.labkey.api.util.URLHelper;
import org.labkey.api.view.ActionURL;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.mcp.annotation.McpResource;
@@ -42,14 +46,107 @@
import org.springframework.ai.tool.annotation.ToolParam;
import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class CoreMcp implements McpService.McpImpl
{
+ // Lazily created, then reused for the lifetime of this instance -- see McpToolProxy's class javadoc.
+ private volatile McpToolProxy documentationProxy;
+
+ public CoreMcp()
+ {
+ }
+
+ private static boolean isDocumentationSourceRemote()
+ {
+ if (DocumentationService.get() instanceof DocumentationService s && s.isEnabled())
+ return false;
+ var documentationServer = AppProps.getInstance().getDocumentationServer();
+ if (isBlank(documentationServer))
+ return false;
+
+ var baseServerUrl = AppProps.getInstance().getBaseServerUrl();
+ try
+ {
+ return !(new URLHelper(documentationServer).getHost().equalsIgnoreCase(new URLHelper(baseServerUrl).getHost()));
+ }
+ catch (URISyntaxException x)
+ {
+ return false;
+ }
+ }
+
+ private McpToolProxy getDocumentationProxy()
+ {
+ McpToolProxy proxy = documentationProxy;
+ if (proxy == null)
+ {
+ synchronized (this)
+ {
+ proxy = documentationProxy;
+ if (proxy == null)
+ {
+ proxy = new McpToolProxy(AppProps.getInstance().getDocumentationServer());
+ documentationProxy = proxy;
+ }
+ }
+ }
+ return proxy;
+ }
+
+ private String forward(String remoteToolName, Map arguments)
+ {
+ return getDocumentationProxy().forward(remoteToolName, arguments);
+ }
+
+
+ @Tool(description = "Search the LabKey documentation for chunks of text semantically similar to a natural language query. " +
+ "Each result is an excerpt from a larger document, not the full document -- multiple results may come from the same " +
+ "source document. Returns each chunk's content, metadata (title, source URL, content type), a similarity score, and " +
+ "an id identifying the source document; pass that id to retrieveDocument to fetch the entire document.")
+ @RequiresNoPermission
+ String searchDocumentation(
+ @ToolParam(description = "Natural language search query describing what you're looking for") String query,
+ @ToolParam(required = false, description = "Maximum number of results to return, defaults to 5") Integer topK)
+ {
+ if (isDocumentationSourceRemote())
+ {
+ Map arguments = new HashMap<>();
+ arguments.put("query", query);
+ if (topK != null)
+ arguments.put("topK", topK);
+ return forward("searchDocumentation", arguments);
+ }
+
+ DocumentationService svc = DocumentationService.get();
+ if (svc == null || !svc.isEnabled())
+ throw new McpException("Documentation search is not available on this server.");
+ return svc.searchDocumentation(query, topK);
+ }
+
+ @Tool(description = "Return the entire document from the LabKey documentation using the `id` as returned by `searchDocumentation`.")
+ @RequiresNoPermission
+ String retrieveDocument(
+ @ToolParam(description = "Id of the document to return") String id)
+ {
+ if (isDocumentationSourceRemote())
+ {
+ return forward("retrieveDocument", Map.of("id", id));
+ }
+
+ DocumentationService svc = DocumentationService.get();
+ if (svc == null || !svc.isEnabled())
+ throw new McpException("Documentation retrieval is not available on this server.");
+ return svc.retrieveDocument(id);
+ }
+
@Tool(description = "This tool provides useful context information about the current user (name, userid), webserver " +
"(name, url, description), and current container/folder (name, path, url, description) once the container is set via setContainer.")
@RequiresPermission(ReadPermission.class)
diff --git a/core/src/org/labkey/core/DataAnalysis_Python.md b/core/src/org/labkey/core/DataAnalysis_Python.md
index 56d82d58230..311a4287e11 100644
--- a/core/src/org/labkey/core/DataAnalysis_Python.md
+++ b/core/src/org/labkey/core/DataAnalysis_Python.md
@@ -23,7 +23,7 @@ When writing Python scripts, read the server URL from `.mcp.json` to pre-populat
Confirm all of these settings with the analyst before writing any script.
## Online Reference Material
-https://www.labkey.org/Documentation/wiki-page.view?name=python
+For questions about the `labkey` Python package, LabKey SQL, or server concepts not covered here, call `searchDocumentation` (e.g. `searchDocumentation("labkey python APIWrapper select_rows filters")`), then `retrieveDocument` for the full page.
## MCP Tools Available
@@ -291,7 +291,7 @@ All modification APIs accept `timeout=300`, `container_path=None`, `transacted=T
10. **select_rows sends a GET request** to `query-getQuery.api`. `execute_sql` sends a POST to `query-executeSql.api`.
-11. **LabKey SQL is not standard SQL.** It is a SQL dialect specific to LabKey. Use `mcp__labkey__validateSQL` to check syntax before executing. Refer to LabKey documentation for dialect-specific features (e.g., lookup column traversal via `/` or `.` notation).
+11. **LabKey SQL is not standard SQL.** It is a SQL dialect specific to LabKey. Use `mcp__labkey__validateSQL` to check syntax before executing. For dialect-specific features (e.g., lookup column traversal via `/` or `.` notation), read the LabKey SQL resource (`resource://org/labkey/query/controllers/prompts/LabKeySql.md`) or call `searchDocumentation`.
## Typical Analysis Workflow
diff --git a/core/src/org/labkey/core/DataAnalysis_R.md b/core/src/org/labkey/core/DataAnalysis_R.md
index 61d72321f99..225b656c106 100644
--- a/core/src/org/labkey/core/DataAnalysis_R.md
+++ b/core/src/org/labkey/core/DataAnalysis_R.md
@@ -23,7 +23,7 @@ When writing R scripts, read the server URL from `.mcp.json` (via `jsonlite::fro
Confirm all of these settings with the analyst before writing any script.
## Online Reference Material
-https://www.labkey.org/Documentation/wiki-page.view?name=rAPI
+For questions about the Rlabkey package, LabKey SQL, or server concepts not covered here, call `searchDocumentation` (e.g. `searchDocumentation("Rlabkey selectRows executeSql filters")`), then `retrieveDocument` for the full page.
## MCP Tools Available
@@ -420,7 +420,7 @@ Data frames passed to modification functions must be created with `stringsAsFact
10. **baseUrl must include the context path and trailing slash**: e.g. `"http://localhost:8080/labkey/"` if the server uses a context path, or `"http://localhost:8080/"` if it does not.
-11. **LabKey SQL is not standard SQL.** It is a SQL dialect specific to LabKey. Use `mcp__labkey__validateSQL` to check syntax before executing. Refer to LabKey documentation for dialect-specific features (e.g., lookup column traversal via `/` or `.` notation).
+11. **LabKey SQL is not standard SQL.** It is a SQL dialect specific to LabKey. Use `mcp__labkey__validateSQL` to check syntax before executing. For dialect-specific features (e.g., lookup column traversal via `/` or `.` notation), read the LabKey SQL resource (`resource://org/labkey/query/controllers/prompts/LabKeySql.md`) or call `searchDocumentation`.
12. **SSL configuration**: For HTTPS servers on Windows, you may need to set the `RLABKEY_CAINFO_FILE` environment variable pointing to a CA bundle file. Use `labkey.acceptSelfSignedCerts()` for development servers with self-signed certificates.
diff --git a/core/src/org/labkey/core/FileBasedModules.md b/core/src/org/labkey/core/FileBasedModules.md
index 20fc8811fb2..c97cb1eaf66 100644
--- a/core/src/org/labkey/core/FileBasedModules.md
+++ b/core/src/org/labkey/core/FileBasedModules.md
@@ -423,12 +423,8 @@ Simply refresh your browser to see changes.
## Documentation Resources
-For more information, see:
-- Simple Modules Overview: https://www.labkey.org/Documentation/wiki-page.view?name=simpleModules
-- File-Based Module Tutorial: https://www.labkey.org/Documentation/wiki-page.view?name=moduleqvr
- JavaScript API Documentation: https://labkey.github.io/labkey-api-js/
-- Module Directory Structures: https://www.labkey.org/Documentation/wiki-page.view?name=moduleDirectoryStructures
-- Query Development: https://www.labkey.org/Documentation/wiki-page.view?name=addSQLQuery
+- For everything else (simple modules overview, the file-based module tutorial, module directory structures, query development), call `searchDocumentation` — e.g. `searchDocumentation("file-based module directory structure")` — then `retrieveDocument` for the full page.
## Quick Start Checklist
diff --git a/core/src/org/labkey/core/Reports.md b/core/src/org/labkey/core/Reports.md
index 6f3f1b1d5c8..1f8bea765d5 100644
--- a/core/src/org/labkey/core/Reports.md
+++ b/core/src/org/labkey/core/Reports.md
@@ -8,6 +8,8 @@ Jump to the track that matches your language:
Everything else on this page (data-bound vs. not, authorization, UI vs. file-based module) applies to both languages.
+For report topics not covered here (e.g. additional export formats, less common report types), call `searchDocumentation`.
+
## Report Type Landscape
LabKey has several built-in report types: Query Report (renders a query view, no script), Attachment Report (uploaded static document), Link Report (URL pointer), JavaScript Report (runs in the *viewer's browser*, not server-side), R Report, Jupyter Report, and Query Snapshot (a persisted table, not really a "report"). **R Reports** and **Jupyter Reports** are the two paths this guide covers for turning an analyst-authored script into a server-side report. (A generic `ExternalScriptEngineReport`/`InternalScriptEngineReport` mechanism also exists for other JSR223-compatible engines an admin configures — historically used for Perl — but there's no conversion track for it here.)
diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java
index 97175dc35ba..68cac5c178d 100644
--- a/core/src/org/labkey/core/admin/AdminController.java
+++ b/core/src/org/labkey/core/admin/AdminController.java
@@ -926,6 +926,12 @@ public static ActionURL getDeprecatedFeaturesURL()
{
return new ActionURL(OptionalFeaturesAction.class, ContainerManager.getRoot()).addParameter("type", FeatureType.Deprecated.name());
}
+
+ @Override
+ public ActionURL getOptionalFeaturesURL()
+ {
+ return new ActionURL(OptionalFeaturesAction.class, ContainerManager.getRoot()).addParameter("type", FeatureType.Optional.name());
+ }
}
public static class MaintenanceBean
diff --git a/core/src/org/labkey/core/webdav/DavController.java b/core/src/org/labkey/core/webdav/DavController.java
index f171e828460..a62f976dcb0 100644
--- a/core/src/org/labkey/core/webdav/DavController.java
+++ b/core/src/org/labkey/core/webdav/DavController.java
@@ -3930,8 +3930,10 @@ WebdavStatus doMethod() throws DavException, IOException, RedirectException
return getResponse().sendError(WebdavStatus.SC_LOCKED);
}
- Path path1 = getResourcePath();
-// _log.info("LOCK " + path1.toString()); // TODO: TEMP logging
+ if (getUser().isGuest())
+ {
+ return getResponse().sendError(WebdavStatus.SC_FORBIDDEN);
+ }
LockInfo lock = new LockInfo();
diff --git a/query/src/org/labkey/query/controllers/prompts/LabKeySql.md b/query/src/org/labkey/query/controllers/prompts/LabKeySql.md
index 2346ea35687..ce98cb23e71 100644
--- a/query/src/org/labkey/query/controllers/prompts/LabKeySql.md
+++ b/query/src/org/labkey/query/controllers/prompts/LabKeySql.md
@@ -364,3 +364,5 @@ Many parse errors now include an inline suggestion (e.g. `Syntax error near 'OFF
| `Parameter type is not supported: DATE` | Use TIMESTAMP (§11) |
| `The underlying database does not support nested ORDER BY unless LIMIT...` (warning) | ORDER BY is being dropped — add `LIMIT` or sort via the API |
| `ExecutingSelector; bad SQL grammar []` | Runtime (database-level) failure with no detail — usually a type mismatch (add CASTs) or a dialect-specific function limit (e.g. `timestampdiff` YEAR on PostgreSQL, `sum` over text) |
+
+For any LabKey SQL topic not covered above, call `searchDocumentation`.