From d361e34d58937cfee8aa5330633e737420cdabea Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Wed, 1 Jul 2026 11:17:30 -0700 Subject: [PATCH 01/10] Documentation MCP (#7805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Rationale Support indexing wikis ## Related Pull Requests ## Changes - --------- Co-authored-by: Adam Rauch --- api/src/org/labkey/api/ApiModule.java | 2 + api/src/org/labkey/api/mcp/McpService.java | 22 ++++ .../org/labkey/api/mcp/NoopMcpService.java | 21 ++++ .../org/labkey/api/wiki/WikiRendererType.java | Bin 3537 -> 18608 bytes api/src/org/labkey/api/wiki/WikiService.java | 23 +++++ core/module.properties | 26 ++--- .../postgresql/core-26.005-26.006.sql | 1 + core/src/org/labkey/core/CoreMcp.java | 29 +++++- core/src/org/labkey/core/CoreModule.java | 4 +- .../org/labkey/devtools/TestController.java | 95 ------------------ .../labkey/query/controllers/QueryMcp.java | 2 +- wiki/src/org/labkey/wiki/WikiManager.java | 86 ++++++++++++++++ 12 files changed, 198 insertions(+), 113 deletions(-) create mode 100644 core/resources/schemas/dbscripts/postgresql/core-26.005-26.006.sql diff --git a/api/src/org/labkey/api/ApiModule.java b/api/src/org/labkey/api/ApiModule.java index 83170e3529d..1a0ebb21c07 100644 --- a/api/src/org/labkey/api/ApiModule.java +++ b/api/src/org/labkey/api/ApiModule.java @@ -189,6 +189,7 @@ import org.labkey.api.view.ViewServlet; import org.labkey.api.view.WebPartFactory; import org.labkey.api.webdav.WebdavResolverImpl; +import org.labkey.api.wiki.WikiRendererType; import org.labkey.api.writer.ContainerUser; import org.labkey.filters.ContentSecurityPolicyFilter; @@ -562,6 +563,7 @@ public void registerServlets(ServletContext servletCtx) UserManager.TestCase.class, ViewCategoryManager.TestCase.class, WebdavResolverImpl.TestCase.class, + WikiRendererType.TestCase.class, WorkbookContainerType.TestCase.class, WriteableLookAndFeelProperties.TestCase.class ); diff --git a/api/src/org/labkey/api/mcp/McpService.java b/api/src/org/labkey/api/mcp/McpService.java index 3841c9ae145..4717e859b78 100644 --- a/api/src/org/labkey/api/mcp/McpService.java +++ b/api/src/org/labkey/api/mcp/McpService.java @@ -36,6 +36,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.Supplier; /// @@ -94,6 +95,8 @@ /// public interface McpService extends ToolCallbackProvider { + String VECTOR_SCHEMA = "vector_indexes"; // for pgvector extension, see core-26.005-26.006.sql + Logger LOG = LogHelper.getLogger(McpService.class, "MCP registration exceptions"); String ENABLE_MCP_SERVER_FLAG = "enableMcpServer"; @@ -187,6 +190,8 @@ default ChatClient getChat(HttpSession session, String conversationName, Supplie record MessageResponse(String contentType, String text, HtmlString html) {} + record VectorDocument(String id, String text, Map metadata) {} + /** get a consolidated response (good for many text-oriented agents/use-cases) */ MessageResponse sendMessage(ChatClient chat, String message); @@ -201,4 +206,21 @@ default List sendMessageEx(ChatClient chat, String message) * CONSIDER: Is it possible to implement VectorStoreRetriever wrapper for SearchService??? */ VectorStore getVectorStore(); + + /** Returns true if the vector store exists and contains at least one document. */ + boolean isVectorStorePopulated(@NotNull VectorStore vs); + + /** + * Adds documents to the vector store, automatically splitting any document whose token + * count exceeds the embedding model's input limit. Prefer this over + * {@code getVectorStore().add(...)} for indexing — it prevents the + * {@code IllegalArgumentException} that {@code TokenCountBatchingStrategy} throws on + * oversized inputs. + */ + void addDocuments(List documents); + + void saveVectorStore(); + + /** Drop and recreate the vector store table. Use when the embedding model has changed and dimensions no longer match. */ + void resetVectorStore(); } diff --git a/api/src/org/labkey/api/mcp/NoopMcpService.java b/api/src/org/labkey/api/mcp/NoopMcpService.java index e33eb422a4c..2a0ff1c2f9a 100644 --- a/api/src/org/labkey/api/mcp/NoopMcpService.java +++ b/api/src/org/labkey/api/mcp/NoopMcpService.java @@ -101,4 +101,25 @@ public VectorStore getVectorStore() { return null; } + + @Override + public boolean isVectorStorePopulated(@NotNull VectorStore vs) + { + return false; + } + + @Override + public void addDocuments(List documents) + { + } + + @Override + public void saveVectorStore() + { + } + + @Override + public void resetVectorStore() + { + } } diff --git a/api/src/org/labkey/api/wiki/WikiRendererType.java b/api/src/org/labkey/api/wiki/WikiRendererType.java index b390a9e58cb12ac80e76a75493bcfa07a5911601..9c4acae5ccfa1873f81235ae97d5e298d2d2c262 100644 GIT binary patch literal 18608 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 N_C+7WZS@CN{|A#zD!Tvx delta 44 zcmV+{0Mq}lkpa;gu%7~x?E?#wd;_zy-vnU-vtAQi0+U`72(v;VzXG$wB<%tNeGLkb CVGvRP diff --git a/api/src/org/labkey/api/wiki/WikiService.java b/api/src/org/labkey/api/wiki/WikiService.java index 40d6a1210d7..3dfb4efadc1 100644 --- a/api/src/org/labkey/api/wiki/WikiService.java +++ b/api/src/org/labkey/api/wiki/WikiService.java @@ -50,6 +50,15 @@ record RenderedWiki (String name, String title, HtmlString html, String entityId RenderedWiki getRenderedWiki(Container c, String name); + record WikiMarkdown(String name, String title, String markdown, String entityId) {} + + /** + * Returns a best-effort Markdown rendering of the wiki's raw source, intended for indexing + * (search, embedding, vector stores) — NOT for user display. Conversion is lossy and may + * drop or mangle markup details that don't have a direct Markdown equivalent. + */ + WikiMarkdown getWikiMarkdown(Container c, String name); + // Quick check for a terms-of-use wiki in the provided container. Helps optimize the every-request terms check. boolean hasTermsOfUseWiki(Container c); @@ -108,4 +117,18 @@ default HtmlString getHtml(Container c, String name) String updateAttachments(Container c, User user, String wikiName, @Nullable List attachmentFiles, @Nullable List deleteAttachmentNames); AttachmentParentType getAttachmentType(); + + /** + * Loads all wikis from the given container into the MCP vector store. + * + *

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.

+ * + * @return the number of documents added + */ + int populateVectorStore(Container container); } diff --git a/core/module.properties b/core/module.properties index d4c7dec2745..0be23cf2635 100644 --- a/core/module.properties +++ b/core/module.properties @@ -1,13 +1,13 @@ -Name: Core -ModuleClass: org.labkey.core.CoreModule -SchemaVersion: 26.005 -Label: Administration and Essential Services -Description: The Core module provides central services such as login, \ - security, administration, folder management, user management, \ - module upgrade, file attachments, analytics, and portal page management. -Organization: LabKey -OrganizationURL: https://www.labkey.com/ -License: Apache 2.0 -LicenseURL: http://www.apache.org/licenses/LICENSE-2.0 -SupportedDatabases: mssql, pgsql -ManageVersion: true +Name: Core +ModuleClass: org.labkey.core.CoreModule +SchemaVersion: 26.006 +Label: Administration and Essential Services +Description: The Core module provides central services such as login, \ + security, administration, folder management, user management, \ + module upgrade, file attachments, analytics, and portal page management. +Organization: LabKey +OrganizationURL: https://www.labkey.com/ +License: Apache 2.0 +LicenseURL: http://www.apache.org/licenses/LICENSE-2.0 +SupportedDatabases: mssql, pgsql +ManageVersion: true diff --git a/core/resources/schemas/dbscripts/postgresql/core-26.005-26.006.sql b/core/resources/schemas/dbscripts/postgresql/core-26.005-26.006.sql new file mode 100644 index 00000000000..40ae6f63620 --- /dev/null +++ b/core/resources/schemas/dbscripts/postgresql/core-26.005-26.006.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS "vector_indexes"; \ No newline at end of file diff --git a/core/src/org/labkey/core/CoreMcp.java b/core/src/org/labkey/core/CoreMcp.java index d85fa45dca9..e54bb6118fd 100644 --- a/core/src/org/labkey/core/CoreMcp.java +++ b/core/src/org/labkey/core/CoreMcp.java @@ -19,11 +19,13 @@ import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; import org.json.JSONObject; import org.labkey.api.collections.LabKeyCollectors; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; import org.labkey.api.mcp.McpService; +import org.labkey.api.module.ModuleLoader; import org.labkey.api.security.RequiresNoPermission; import org.labkey.api.security.RequiresPermission; import org.labkey.api.security.User; @@ -145,11 +147,32 @@ String setContainer(ToolContext context, @ToolParam(description = "Container pat return message; } + + // TODO replace/augment with available feature list + @Tool(description = "List the modules installed on this server, this may be useful in inferring the available functionality. For instance, " + + "the presence of the `premium` module implies the availability of premium featues.") + @RequiresNoPermission + public String listModules(ToolContext context) + { + JSONArray modules = new JSONArray(); + ModuleLoader.getInstance().getModules().stream() + .map(module -> { + JSONObject obj = new JSONObject(); + obj.put("name", module.getName()); + if (StringUtils.isNotEmpty(module.getLabel())) + obj.put("label", module.getLabel()); + return obj; + }) + .forEach(modules::put); + return new JSONObject(Map.of("modules",modules)).toString(); + } + + @McpResource( uri = "resource://org/labkey/core/FileBasedModules.md", mimeType = "application/markdown", name = "File-Based Module Development Guide", - description = "Provide documentation for developing LabKey file-based modules") + description = "Required reading before building file-based modules. Covers module.properties, directory structure, web parts, SQL queries, reports, and deployment.") public ReadResourceResult getFileBasedModuleDevelopmentGuide() throws IOException { incrementResourceRequestCount("File-Based Modules"); @@ -167,7 +190,7 @@ public ReadResourceResult getFileBasedModuleDevelopmentGuide() throws IOExceptio uri = "resource://org/labkey/core/DataAnalysis_Python.md", mimeType = "application/markdown", name = "Python Data Analysis Development Guide", - description = "Provide documentation for developers using Python to analyze LabKey data") + description = "Required reading before writing Python scripts. Covers APIWrapper setup, .netrc auth, select_rows, execute_sql, QueryFilter, and pandas workflows.") public ReadResourceResult getPythonDataAnalysisGuide() throws IOException { incrementResourceRequestCount("Python Data Analysis"); @@ -185,7 +208,7 @@ public ReadResourceResult getPythonDataAnalysisGuide() throws IOException uri = "resource://org/labkey/core/DataAnalysis_R.md", mimeType = "application/markdown", name = "R Data Analysis Development Guide", - description = "Provide documentation for developers using R to analyze LabKey data") + description = "Required reading before writing R scripts. Covers Rlabkey setup, .netrc auth, labkey.selectRows, labkey.executeSql, makeFilter, and data frame workflows.") public ReadResourceResult getRDataAnalysisGuide() throws IOException { incrementResourceRequestCount("R Data Analysis"); diff --git a/core/src/org/labkey/core/CoreModule.java b/core/src/org/labkey/core/CoreModule.java index 8d87668be99..657a3dd5640 100644 --- a/core/src/org/labkey/core/CoreModule.java +++ b/core/src/org/labkey/core/CoreModule.java @@ -368,6 +368,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.labkey.api.mcp.McpService.VECTOR_SCHEMA; import static org.labkey.api.settings.StashedStartupProperties.homeProjectFolderType; import static org.labkey.api.settings.StashedStartupProperties.homeProjectResetPermissions; import static org.labkey.api.settings.StashedStartupProperties.homeProjectWebparts; @@ -1556,7 +1557,8 @@ public Collection getSchemaNames() CoreSchema.getInstance().getSchemaName(), // core PropertySchema.getInstance().getSchemaName(), // prop TestSchema.getInstance().getSchemaName(), // test - DbSchema.TEMP_SCHEMA_NAME // temp + DbSchema.TEMP_SCHEMA_NAME, // temp + VECTOR_SCHEMA // used by pgvector - see core-26.005-26.006.sql ); } diff --git a/devtools/src/org/labkey/devtools/TestController.java b/devtools/src/org/labkey/devtools/TestController.java index 5de66de2919..c7171d20189 100644 --- a/devtools/src/org/labkey/devtools/TestController.java +++ b/devtools/src/org/labkey/devtools/TestController.java @@ -18,13 +18,11 @@ import jakarta.servlet.http.HttpServletResponse; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.Assert; import org.junit.Test; import org.labkey.api.action.ApiResponse; import org.labkey.api.action.ApiSimpleResponse; -import org.labkey.api.action.ConfirmAction; import org.labkey.api.action.FormArrayList; import org.labkey.api.action.FormViewAction; import org.labkey.api.action.JsonInputLimit; @@ -36,7 +34,6 @@ import org.labkey.api.action.SimpleResponse; import org.labkey.api.action.SimpleViewAction; import org.labkey.api.action.SpringActionController; -import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; import org.labkey.api.data.DbScope; import org.labkey.api.data.SQLFragment; @@ -63,9 +60,7 @@ import org.labkey.api.util.ConfigurationException; import org.labkey.api.util.DOM; import org.labkey.api.util.ExceptionUtil; -import org.labkey.api.util.FileUtil; import org.labkey.api.util.HtmlString; -import org.labkey.api.util.HtmlStringBuilder; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.TestContext; import org.labkey.api.util.URLHelper; @@ -80,15 +75,10 @@ import org.labkey.api.view.ViewServlet; import org.labkey.api.view.template.ClientDependency; import org.labkey.api.view.template.PageConfig; -import org.labkey.api.wiki.WikiService; -import org.springframework.ai.document.Document; -import org.springframework.ai.vectorstore.SimpleVectorStore; -import org.springframework.ai.vectorstore.VectorStore; import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.validation.BindException; import org.springframework.validation.Errors; -import org.springframework.validation.ObjectError; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.Controller; @@ -98,9 +88,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Gatherers; import static org.labkey.api.util.DOM.Attribute.name; import static org.labkey.api.util.DOM.Attribute.src; @@ -1328,88 +1315,6 @@ protected String getServicePrompt() } } - - @RequiresLogin - public static class PopulateVectorStoreAction extends ConfirmAction - { - AtomicInteger count = new AtomicInteger(); - - @Override - public ModelAndView getConfirmView(Object o, BindException errors) - { - var db = FileUtil.getTempDirectoryFileLike().resolveChild("VectorStore.database"); - HtmlStringBuilder message = HtmlStringBuilder.of(); - message.append("This will add the contents of /Documention wikis to the vector store.").append(HtmlString.BR); - message.append("This may take a few minutes."); - if (db.exists()) - message.unsafeAppend("

").append("I see a vector store file already exists. Just FYI."); - return new HtmlView(message); - } - - @Override - public void validateCommand(Object o, Errors errors) - { - } - - @Override - public @NotNull URLHelper getSuccessURL(Object o) - { - return null; - } - - - // not usually used but some actions return views that close the current window etc... - @Override - public ModelAndView getSuccessView(Object form) - { - return HtmlView.of(count.get() + " documents added to vector store"); - } - - @Override - public boolean handlePost(Object o, BindException errors) - { - Container documentsContainer = ContainerManager.getForPath("/Documentation"); - if (null == documentsContainer) - throw new NotFoundException(); - VectorStore vs = McpService.get().getVectorStore(); - if (null == vs) - throw new NotFoundException("/Documentation project was not found"); - - ActionURL wikiBase = new ActionURL("wiki","page",documentsContainer); - - WikiService service = Objects.requireNonNull(WikiService.get()); - List all = service.getNames(documentsContainer); - all.stream() - .map(name -> service.getRenderedWiki(documentsContainer, name)) - .filter(Objects::nonNull) - .map(wiki -> - { - count.incrementAndGet(); - var metadata = Map.of( - "Content-Type", "text/html", - "filename", wiki.name() + ".html", - "title", (Object)wiki.title(), - "source", wikiBase.clone().addParameter("name",wiki.name()).getURIString() - ); - return new Document(wiki.entityId(), wiki.html().toString(), metadata); - }) - .gather(Gatherers.windowFixed(50)) - .forEach(vs); - - var db = FileUtil.getTempDirectoryFileLike().resolveChild("VectorStore.database"); - try - { - ((SimpleVectorStore)vs).save(db.toNioPathForRead().toFile()); - return true; - } - catch (Exception x) - { - errors.addError(new ObjectError("form", "error saving vectordb: " + x.getMessage())); - return false; - } - } - } - @RequiresPermission(UpdatePermission.class) public static class ExecuteMutatingSqlGetAction extends SimpleViewAction { diff --git a/query/src/org/labkey/query/controllers/QueryMcp.java b/query/src/org/labkey/query/controllers/QueryMcp.java index 07e28cee290..8cc5cd53603 100644 --- a/query/src/org/labkey/query/controllers/QueryMcp.java +++ b/query/src/org/labkey/query/controllers/QueryMcp.java @@ -67,7 +67,7 @@ public class QueryMcp implements McpService.McpImpl uri = "resource://org/labkey/query/controllers/prompts/LabKeySql.md", mimeType = "application/markdown", name = "LabKey SQL", - description = "Provide documentation for LabKey SQL specific syntax") + description = "Required reading before writing or debugging LabKey SQL. Covers lookup traversal, PIVOT, parameterized queries, cross-folder queries, and all available functions.") public ReadResourceResult getLabKeySQLDocumentation() throws IOException { incrementResourceRequestCount("LabKey SQL"); diff --git a/wiki/src/org/labkey/wiki/WikiManager.java b/wiki/src/org/labkey/wiki/WikiManager.java index 8a880270223..dae042c2411 100644 --- a/wiki/src/org/labkey/wiki/WikiManager.java +++ b/wiki/src/org/labkey/wiki/WikiManager.java @@ -42,6 +42,8 @@ import org.labkey.api.data.Table; import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; +import org.labkey.api.mcp.McpService; +import org.labkey.api.mcp.McpService.VectorDocument; import org.labkey.api.query.FieldKey; import org.labkey.api.query.QueryService; import org.labkey.api.search.SearchService; @@ -55,6 +57,7 @@ import org.labkey.api.util.TestContext; import org.labkey.api.view.ActionURL; import org.labkey.api.view.HtmlView; +import org.labkey.api.view.NotFoundException; import org.labkey.api.view.NavTree; import org.labkey.api.view.Portal; import org.labkey.api.view.ViewContext; @@ -91,6 +94,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; import static org.labkey.api.action.SpringActionController.ERROR_MSG; import static org.labkey.api.security.WikiTermsOfUseProvider.TERMS_OF_USE_WIKI_NAME; @@ -869,6 +873,28 @@ public RenderedWiki getRenderedWiki(Container c, String name) } } + @Override + public WikiMarkdown getWikiMarkdown(Container c, String name) + { + if (null == c || null == name) + return null; + + try + { + Wiki wiki = WikiSelectManager.getWiki(c, name); + if (null == wiki) + return null; + WikiVersion version = wiki.getLatestVersion(); + String body = version.getBody(); + String markdown = version.getRendererTypeEnum().bestAttemptConvertToMarkdown(null == body ? "" : body); + return new WikiMarkdown(name, version.getTitle(), markdown, wiki.getEntityId()); + } + catch (Exception x) + { + throw new RuntimeException(x); + } + } + @Override public boolean hasTermsOfUseWiki(Container c) { @@ -938,6 +964,66 @@ public List getNames(Container c) return new ArrayList<>(l); } + @Override + public int populateVectorStore(Container container) + { + McpService mcp = McpService.get(); + if (null == mcp.getVectorStore()) + throw new NotFoundException("VectorStore not enabled."); + + ActionURL wikiBase = new ActionURL("wiki", "page", container); + AtomicInteger count = new AtomicInteger(); + + for (String name : getNames(container)) + { + Wiki wiki = WikiSelectManager.getWiki(container, name); + if (null == wiki) + continue; + WikiVersion version = wiki.getLatestVersion(); + if (null == version) + continue; + + String body = version.getBody(); + String markdown = version.getRendererTypeEnum().bestAttemptConvertToMarkdown(null == body ? "" : body); + List path = getAncestorTitles(wiki); + + Map metadata = new HashMap<>(); + metadata.put("Content-Type", "text/markdown"); + metadata.put("filename", name + ".md"); + metadata.put("title", version.getTitle()); + metadata.put("source", wikiBase.clone().addParameter("name", name).getURIString()); + if (!path.isEmpty()) + metadata.put("path", path); + + VectorDocument doc = new VectorDocument(container.getId() + "/" + wiki.getEntityId(), markdown, metadata); + try + { + mcp.addDocuments(List.of(doc)); + count.incrementAndGet(); + } + catch (IllegalArgumentException x) + { + LogManager.getLogger(WikiManager.class).info(name, x); + } + } + + mcp.saveVectorStore(); + return count.get(); + } + + private List getAncestorTitles(Wiki wiki) + { + List titles = new ArrayList<>(); + Wiki current = wiki.getParentWiki(); + while (current != null) + { + WikiVersion version = current.getLatestVersion(); + titles.add(0, version != null ? version.getTitle() : current.getName()); + current = current.getParentWiki(); + } + return titles; + } + @Override public void addWikiListener(WikiChangeListener listener) { From 56326d1a9e53505d5e21d874c7427e24e7d9e36d Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Thu, 2 Jul 2026 15:29:14 -0700 Subject: [PATCH 02/10] canEditSourceModule() consistency (#7727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### Rationale null check #### Related Pull Requests * https://github.com/LabKey/premiumModules/pull/596 * https://github.com/LabKey/platform/pull/7727 * https://github.com/LabKey/testAutomation/pull/3085 #### Changes - --------- Co-authored-by: labkey-tchad --- api/src/org/labkey/api/admin/AdminBean.java | 6 ------ core/src/org/labkey/core/admin/AdminController.java | 2 +- core/src/org/labkey/core/admin/admin.jsp | 7 ++++++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/api/src/org/labkey/api/admin/AdminBean.java b/api/src/org/labkey/api/admin/AdminBean.java index 62631713f3c..8b928e6ba07 100644 --- a/api/src/org/labkey/api/admin/AdminBean.java +++ b/api/src/org/labkey/api/admin/AdminBean.java @@ -24,7 +24,6 @@ import org.labkey.api.data.ContainerManager; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.DbScope; -import org.labkey.api.module.Module; import org.labkey.api.module.ModuleLoader; import org.labkey.api.security.UserManager; import org.labkey.api.settings.AppProps; @@ -43,7 +42,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -82,7 +80,6 @@ public static class RecentUser public static final String sessionTimeout = Formats.commaf0.format(ModuleLoader.getServletContext().getSessionTimeout()); public static final String buildTime = ModuleLoader.getInstance().getCoreModule().getBuildTime(); public static final String serverStartupTime = DateUtil.formatDateTime(ContainerManager.getRoot()); - public static final List modules; public static String asserts = "disabled"; @@ -116,9 +113,6 @@ public static class RecentUser //noinspection ConstantConditions,AssertWithSideEffects assert null != (asserts = "enabled"); - - modules = new ArrayList<>(ModuleLoader.getInstance().getModules()); - modules.sort(Comparator.comparing(Module::getName, String.CASE_INSENSITIVE_ORDER)); } private static @Nullable String getValue(Field field) diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index 6adb7f6825d..98df842b77c 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -9193,7 +9193,7 @@ protected void renderView(Object model, HtmlWriter out) if (null != module.getZippedPath()) p = module.getZippedPath().toPath(); if (isDevMode && ModuleEditorService.get().canEditSourceModule(module)) - if (!module.getExplodedPath().getPath().equals(module.getSourcePath())) + if (!isBlank(module.getSourcePath()) && !module.getExplodedPath().getPath().equals(module.getSourcePath())) p = Paths.get(module.getSourcePath()); fullPathToModule = p.toString(); shortPathToModule = fullPathToModule; diff --git a/core/src/org/labkey/core/admin/admin.jsp b/core/src/org/labkey/core/admin/admin.jsp index 9975bfc4703..81aaa1c7749 100644 --- a/core/src/org/labkey/core/admin/admin.jsp +++ b/core/src/org/labkey/core/admin/admin.jsp @@ -23,6 +23,7 @@ <%@ page import="org.labkey.api.files.FileContentService" %> <%@ page import="org.labkey.api.module.DefaultModule" %> <%@ page import="org.labkey.api.module.Module" %> +<%@ page import="org.labkey.api.module.ModuleLoader"%> <%@ page import="org.labkey.api.moduleeditor.api.ModuleEditorService" %> <%@ page import="org.labkey.api.settings.AdminConsole" %> <%@ page import="org.labkey.api.settings.AdminConsole.AdminLink" %> @@ -36,7 +37,9 @@ <%@ page import="java.time.Duration" %> <%@ page import="java.time.LocalDateTime" %> <%@ page import="java.time.format.DateTimeFormatter" %> +<%@ page import="java.util.ArrayList" %> <%@ page import="java.util.Collection" %> +<%@ page import="java.util.Comparator" %> <%@ page import="java.util.Map" %> <%@ page import="java.util.TreeMap" %> <%@ page import="org.apache.commons.lang3.Strings" %> @@ -152,7 +155,9 @@ <%=link("Module Details", AdminController.ModulesAction.class)%>

<% - for (Module module : AdminBean.modules) + ArrayList modules = new ArrayList<>(ModuleLoader.getInstance().getModules()); + modules.sort(Comparator.comparing(Module::getName, String.CASE_INSENSITIVE_ORDER)); + for (Module module : modules) { String guid = makeId("m_"); String toggleScript = "return LABKEY.Utils.toggleLink(document.getElementById(" + q(guid) + "), false);"; From 8341e1f86caa13f9104db761312a5766f15685fb Mon Sep 17 00:00:00 2001 From: Xing Yang <5168106+XingY@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:54:47 -0700 Subject: [PATCH 03/10] GitHub Issue 1117: sample type and dataclass foreign keys (#7804) --- .../org/labkey/api/assay/AssayResultDomainKind.java | 9 --------- experiment/src/org/labkey/experiment/XarReader.java | 5 +++++ .../org/labkey/experiment/api/SampleTypeServiceImpl.java | 4 ++++ .../experiment/api/property/StorageProvisionerImpl.java | 9 +++++++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java b/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java index 38d42d23233..af87f14db13 100644 --- a/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java +++ b/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java @@ -100,15 +100,6 @@ public Set getPropertyIndices(Domain domain) return PageFlowUtil.set(new PropertyStorageSpec.Index(false, AbstractTsvAssayProvider.DATA_ID_COLUMN_NAME)); } - @Override - public Set getPropertyForeignKeys(Container container) - { - return new HashSet<>(Arrays.asList( - new PropertyStorageSpec.ForeignKey(SpecialColumn.CreatedBy.name(), "core", "users", "userid", null, false), - new PropertyStorageSpec.ForeignKey(SpecialColumn.ModifiedBy.name(), "core", "users", "userid", null, false) - )); - } - @Override public DbScope getScope() { diff --git a/experiment/src/org/labkey/experiment/XarReader.java b/experiment/src/org/labkey/experiment/XarReader.java index 31791c10306..599fdec5394 100644 --- a/experiment/src/org/labkey/experiment/XarReader.java +++ b/experiment/src/org/labkey/experiment/XarReader.java @@ -97,6 +97,7 @@ import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.api.SampleTypeService; import org.labkey.api.exp.property.Domain; +import org.labkey.api.exp.property.DomainKind; import org.labkey.api.exp.property.DomainProperty; import org.labkey.api.exp.property.PropertyService; import org.labkey.api.exp.query.ExpMaterialTable; @@ -893,6 +894,10 @@ private Domain loadDomain(DomainDescriptorType xDomain) throws ExperimentExcepti try { + DomainKind kind = domain.getDomainKind(); + if (kind != null) + domain.setPropertyForeignKeys(kind.getPropertyForeignKeys(getContainer())); // GitHub Issue 1117 + domain.save(getUser()); DefaultValueService.get().setDefaultValues(domain.getContainer(), newDefaultValues); } diff --git a/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java b/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java index 9ba38e5d89b..f69d0948abe 100644 --- a/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java @@ -835,7 +835,11 @@ public ExpSampleTypeImpl createSampleType(Container c, User u, String name, Stri Domain domain = PropertyService.get().createDomain(c, lsid, name, templateInfo); DomainKind kind = domain.getDomainKind(); if (kind != null) + { domain.setDisabledSystemFields(kind.getDisabledSystemFields(disabledSystemField)); + domain.setPropertyForeignKeys(kind.getPropertyForeignKeys(c)); // GitHub Issue 1117 + } + Set reservedNames = kind.getReservedPropertyNames(domain, u); Set reservedPrefixes = kind.getReservedPropertyNamePrefixes(); Set lowerReservedNames = reservedNames.stream().map(String::toLowerCase).collect(Collectors.toSet()); diff --git a/experiment/src/org/labkey/experiment/api/property/StorageProvisionerImpl.java b/experiment/src/org/labkey/experiment/api/property/StorageProvisionerImpl.java index 3b13e69f3ff..21e4632f511 100644 --- a/experiment/src/org/labkey/experiment/api/property/StorageProvisionerImpl.java +++ b/experiment/src/org/labkey/experiment/api/property/StorageProvisionerImpl.java @@ -213,6 +213,15 @@ private String _create(DbScope scope, DomainKind kind, Domain domain, boolean indices.addAll(domain.getPropertyIndices()); change.setIndexedColumns(domain, indices); + /* + * TODO: + * GitHub Issue 1117: Consider the generic fix below for develop. See https://github.com/LabKey/platform/pull/7804 + * Tests may need updates and domains FKs (for example, issues) maybe need to adjusted. + * + * Set foreignKeys = new LinkedHashSet<>(kind.getPropertyForeignKeys(domain.getContainer())); + * foreignKeys.addAll(domain.getPropertyForeignKeys()); + * change.setForeignKeys(foreignKeys); + */ change.setForeignKeys(domain.getPropertyForeignKeys()); try From 648c4e05025fdb8204ed33bfdacbf196df7114e9 Mon Sep 17 00:00:00 2001 From: Nick Kerr Date: Fri, 3 Jul 2026 13:53:55 -0700 Subject: [PATCH 04/10] 1238: Optional feature flag for AI in products (#7811) --- .../org/labkey/api/mcp/AbstractAgentAction.java | 9 +++++++++ api/src/org/labkey/api/mcp/McpService.java | 15 ++++++++++++--- api/src/org/labkey/api/mcp/NoopMcpService.java | 6 ++++++ api/src/org/labkey/api/util/PageFlowUtil.java | 3 +-- .../core/admin/sql/SqlScriptController.java | 4 ++-- .../src/org/labkey/devtools/TestController.java | 2 +- devtools/src/org/labkey/devtools/view/chat.jsp | 6 +----- query/src/org/labkey/query/view/sourceQuery.jsp | 2 +- 8 files changed, 33 insertions(+), 14 deletions(-) diff --git a/api/src/org/labkey/api/mcp/AbstractAgentAction.java b/api/src/org/labkey/api/mcp/AbstractAgentAction.java index 0affd4c5517..a8bb158aab1 100644 --- a/api/src/org/labkey/api/mcp/AbstractAgentAction.java +++ b/api/src/org/labkey/api/mcp/AbstractAgentAction.java @@ -85,9 +85,18 @@ protected String handleEscape(String prompt) @Override public void validateForm(F form, Errors errors) { + if (!McpService.get().isAIFeaturesEnabled()) + { + errors.reject(ERROR_GENERIC, "Agent actions are not available."); + return; + } + String prompt = form.getPrompt(); if (prompt != null && prompt.length() > MAX_PROMPT_CHAR_LENGTH) + { errors.rejectValue("prompt", ERROR_GENERIC, "Prompt cannot exceed " + MAX_PROMPT_CHAR_LENGTH + " characters."); + return; + } // Only honor a client-supplied conversationId if this agent previously issued this session that // id. Otherwise, generate a fresh one. This prevents a same-session caller from splicing into diff --git a/api/src/org/labkey/api/mcp/McpService.java b/api/src/org/labkey/api/mcp/McpService.java index 4717e859b78..06afb51e63a 100644 --- a/api/src/org/labkey/api/mcp/McpService.java +++ b/api/src/org/labkey/api/mcp/McpService.java @@ -99,6 +99,7 @@ public interface McpService extends ToolCallbackProvider Logger LOG = LogHelper.getLogger(McpService.class, "MCP registration exceptions"); String ENABLE_MCP_SERVER_FLAG = "enableMcpServer"; + String ENABLE_AI_FEATURES = "enableAIFeatures"; // Interface for MCP classes that we will "ingest" using Spring annotations. Provides a few helper methods. interface McpImpl @@ -120,9 +121,6 @@ default User getUser(ToolContext toolContext) // Every MCP resource should call this on every invocation default void incrementResourceRequestCount(String resource) { - if (!get().isEnabled()) - throw new RuntimeException("The MCP server is not enabled for external requests. Consider toggling the optional feature flag."); - get().incrementResourceRequestCount(resource); } } @@ -145,8 +143,19 @@ default boolean isEnabled() return OptionalFeatureService.get().isFeatureEnabled(ENABLE_MCP_SERVER_FLAG); } + default boolean isAIFeaturesEnabled() + { + return OptionalFeatureService.get().isFeatureEnabled(ENABLE_AI_FEATURES); + } + boolean isReady(); + // Convenience for in-product AI features: the AI feature flag is on AND the service has started. + default boolean isAIFeaturesReady() + { + return isAIFeaturesEnabled() && isReady(); + } + // Register MCPs in Module.startup() default void register(McpImpl mcp) { diff --git a/api/src/org/labkey/api/mcp/NoopMcpService.java b/api/src/org/labkey/api/mcp/NoopMcpService.java index 2a0ff1c2f9a..1923c770ccd 100644 --- a/api/src/org/labkey/api/mcp/NoopMcpService.java +++ b/api/src/org/labkey/api/mcp/NoopMcpService.java @@ -42,6 +42,12 @@ public boolean isEnabled() return false; } + @Override + public boolean isAIFeaturesEnabled() + { + return false; + } + @Override public boolean isReady() { diff --git a/api/src/org/labkey/api/util/PageFlowUtil.java b/api/src/org/labkey/api/util/PageFlowUtil.java index 520a6c2d171..b4a80bf1bed 100644 --- a/api/src/org/labkey/api/util/PageFlowUtil.java +++ b/api/src/org/labkey/api/util/PageFlowUtil.java @@ -2265,8 +2265,7 @@ public static JSONObject jsInitObject(ContainerUser context, @Nullable PageConfi if (AppProps.getInstance().isOptionalFeatureEnabled(NotificationMenuView.EXPERIMENTAL_NOTIFICATION_MENU) && user != null) json.put("notifications", Map.of("unreadCount", NotificationService.get().getUnreadNotificationCountByUser(null, user.getUserId()))); - if (McpService.get().isEnabled()) - json.put("mcpReady", McpService.get().isReady()); + json.put("mcpReady", McpService.get().isAIFeaturesReady()); JSONObject defaultHeaders = new JSONObject(); defaultHeaders.put("X-ONUNAUTHORIZED", "UNAUTHORIZED"); diff --git a/core/src/org/labkey/core/admin/sql/SqlScriptController.java b/core/src/org/labkey/core/admin/sql/SqlScriptController.java index 575d44a361a..42305e175b7 100644 --- a/core/src/org/labkey/core/admin/sql/SqlScriptController.java +++ b/core/src/org/labkey/core/admin/sql/SqlScriptController.java @@ -1122,7 +1122,7 @@ protected void renderScript(SqlScript script, PrintWriter out) protected void renderButtons(SqlScript script, PrintWriter out) { out.println(PageFlowUtil.button("Reorder Script").href(getScriptURL(ReorderScriptAction.class, script))); - if (McpService.get().isReady()) + if (McpService.get().isAIFeaturesReady()) { out.println( PageFlowUtil.button("Clean Up Script") @@ -1259,7 +1259,7 @@ protected abstract class BaseAIScriptAction extends ScriptAction protected ModelAndView getScriptView(SqlScript script) { McpService mcpService = McpService.get(); - if (mcpService.isReady()) + if (mcpService.isAIFeaturesReady()) { String prompt = getPrompt(DbScope.getLabKeyScope().getSqlDialect(), script); HttpSession session = getViewContext().getSession(); diff --git a/devtools/src/org/labkey/devtools/TestController.java b/devtools/src/org/labkey/devtools/TestController.java index c7171d20189..d4b39982f9b 100644 --- a/devtools/src/org/labkey/devtools/TestController.java +++ b/devtools/src/org/labkey/devtools/TestController.java @@ -1285,7 +1285,7 @@ public static class ChatAction extends SimpleViewAction @Override public ModelAndView getView(Object o, BindException errors) { - if (null == McpService.get() || !McpService.get().isReady()) + if (null == McpService.get() || !McpService.get().isAIFeaturesReady()) return HtmlView.of("Service is not ready yet."); getPageConfig().setTemplate(PageConfig.Template.Dialog); return new JspView<>("/org/labkey/devtools/view/chat.jsp"); diff --git a/devtools/src/org/labkey/devtools/view/chat.jsp b/devtools/src/org/labkey/devtools/view/chat.jsp index 2799776390d..19b8defd8e5 100644 --- a/devtools/src/org/labkey/devtools/view/chat.jsp +++ b/devtools/src/org/labkey/devtools/view/chat.jsp @@ -15,10 +15,6 @@ * limitations under the License. */ %> -<%@ page import="org.labkey.api.util.DOM" %> -<%@ page import="java.util.stream.Stream" %> -<%@ page import="static org.labkey.api.util.DOM.*" %> -<%@ page import="static org.labkey.api.util.DOM.Attribute.*" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %>