Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
43bae3e
For testing/prototyping labkey documentation RAG
labkey-matthewb Apr 29, 2026
8aefc05
Merge remote-tracking branch 'origin/release26.3-SNAPSHOT' into 26.3_…
labkey-matthewb May 1, 2026
2aeea95
Merge remote-tracking branch 'origin/release26.3-SNAPSHOT' into 26.3_…
labkey-matthewb May 11, 2026
9b9b79b
CoreMCP listModules()
labkey-matthewb May 11, 2026
e5136f0
pgvector store support
labkey-matthewb May 11, 2026
eee192c
Merge remote-tracking branch 'origin/release26.3-SNAPSHOT' into 26.3_…
labkey-matthewb May 21, 2026
2ab8e5f
error message
labkey-matthewb May 21, 2026
51b8aa4
load full_index.json if present
labkey-matthewb May 21, 2026
7617edb
paginiation
labkey-matthewb May 21, 2026
e7a4a05
Merge remote-tracking branch 'origin/release26.3-SNAPSHOT' into 26.3_…
labkey-matthewb May 27, 2026
979d798
WikiService.getWikiMarkdown()
labkey-matthewb May 27, 2026
676f69a
TokenTextSplitter
labkey-matthewb May 27, 2026
3a0eedc
Merge remote-tracking branch 'origin/26.3_fb_mcp_document_mcp' into f…
labkey-matthewb May 27, 2026
28e1b27
place-holder schema "vector_indexes"
labkey-matthewb May 27, 2026
1c04b9f
Document MCP
labkey-matthewb May 28, 2026
b08237a
Merge remote-tracking branch 'origin/develop' into fb_mcp_searchDocum…
labkey-matthewb May 29, 2026
211d301
VectorDocument
labkey-matthewb May 29, 2026
11ef921
DocumentationMCP moved to servicetools module
labkey-matthewb May 29, 2026
f6bb658
Merge remote-tracking branch 'origin/develop' into fb_mcp_searchDocum…
labkey-matthewb May 29, 2026
af62290
Minimal usable interface to load documentation wikis into the "defaul…
labkey-matthewb May 30, 2026
478b8af
Merge remote-tracking branch 'origin/develop' into fb_mcp_searchDocum…
labkey-matthewb Jun 2, 2026
732c1ef
Make MCP_INSTRUCTIONS.md short enough to not be truncated. Be more i…
labkey-matthewb Jun 3, 2026
d59559e
isVectorStorePopulated()
labkey-matthewb Jun 3, 2026
09e9fe5
line endings
labkey-matthewb Jun 3, 2026
a7212ba
CoreModule.getSchemaNames()
labkey-matthewb Jun 3, 2026
f2da284
Merge remote-tracking branch 'origin/develop' into fb_mcp_searchDocum…
labkey-matthewb Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/src/org/labkey/api/ApiModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -552,6 +553,7 @@ public void registerServlets(ServletContext servletCtx)
UserManager.TestCase.class,
ViewCategoryManager.TestCase.class,
WebdavResolverImpl.TestCase.class,
WikiRendererType.TestCase.class,
WorkbookContainerType.TestCase.class,
WriteableLookAndFeelProperties.TestCase.class
);
Expand Down
22 changes: 22 additions & 0 deletions api/src/org/labkey/api/mcp/McpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

///
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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<String, Object> metadata) {}

/** get a consolidated response (good for many text-oriented agents/use-cases) */
MessageResponse sendMessage(ChatClient chat, String message);

Expand All @@ -201,4 +206,21 @@ default List<MessageResponse> 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<VectorDocument> documents);

void saveVectorStore();

/** Drop and recreate the vector store table. Use when the embedding model has changed and dimensions no longer match. */
void resetVectorStore();
}
21 changes: 21 additions & 0 deletions api/src/org/labkey/api/mcp/NoopMcpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,25 @@ public VectorStore getVectorStore()
{
return null;
}

@Override
public boolean isVectorStorePopulated(@NotNull VectorStore vs)
{
return false;
}

@Override
public void addDocuments(List<VectorDocument> documents)
{
}

@Override
public void saveVectorStore()
{
}

@Override
public void resetVectorStore()
{
}
}
Binary file modified api/src/org/labkey/api/wiki/WikiRendererType.java
Binary file not shown.
23 changes: 23 additions & 0 deletions api/src/org/labkey/api/wiki/WikiService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

default HtmlString getHtml(Container c, String name)
{
var wiki = getRenderedWiki(c, name);
Expand Down Expand Up @@ -105,4 +114,18 @@ default HtmlString getHtml(Container c, String name)
String updateAttachments(Container c, User user, String wikiName, @Nullable List<AttachmentFile> attachmentFiles, @Nullable List<String> deleteAttachmentNames);

AttachmentParentType getAttachmentType();

/**
* Loads all wikis from the given container into the MCP vector store.
*
* <p>Each {@link org.labkey.api.mcp.McpService.VectorDocument} is assigned an ID of the form
* {@code "<containerEntityId>/<wikiEntityId>"}, 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.</p>
*
* @return the number of documents added
*/
int populateVectorStore(Container container);
}
26 changes: 13 additions & 13 deletions core/module.properties
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE SCHEMA IF NOT EXISTS "vector_indexes";
29 changes: 26 additions & 3 deletions core/src/org/labkey/core/CoreMcp.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 funcitonality. 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");
Expand All @@ -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");
Expand All @@ -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");
Expand Down
4 changes: 3 additions & 1 deletion core/src/org/labkey/core/CoreModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1552,7 +1553,8 @@ public Collection<String> 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
);
}

Expand Down
95 changes: 0 additions & 95 deletions devtools/src/org/labkey/devtools/TestController.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@

import jakarta.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
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;
Expand All @@ -35,7 +33,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.mcp.AbstractAgentAction;
import org.labkey.api.mcp.McpService;
Expand All @@ -57,9 +54,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;
Expand All @@ -74,15 +69,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;

Expand All @@ -92,9 +82,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;
Expand Down Expand Up @@ -1322,88 +1309,6 @@ protected String getServicePrompt()
}
}


@RequiresLogin
public static class PopulateVectorStoreAction extends ConfirmAction<Object>
{
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("<p/><p/>").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<String> 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;
}
}
}

@AdminConsoleAction(AdminOperationsPermission.class)
@JsonInputLimit(50)
public static class TestJsonObjectInputLimitAction extends MutatingApiAction<SimpleApiJsonForm>
Expand Down
2 changes: 1 addition & 1 deletion query/src/org/labkey/query/controllers/QueryMcp.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading