Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d5032b7
MCP server production MVP
labkey-adam Mar 19, 2026
915d2a6
Merge remote-tracking branch 'origin/develop' into fb_mcp_mvp
labkey-adam Mar 19, 2026
5a656f3
Imports
labkey-adam Mar 19, 2026
bf1cf2b
listContainers
labkey-adam Mar 19, 2026
8ee2e9f
Ask user for container path and cache it. Clean up all endpoints.
labkey-adam Mar 19, 2026
6292044
Provide less severe guidance when container is missing
labkey-adam Mar 20, 2026
748f35b
Update Spring AI to 2.0.0-M3
labkey-adam Mar 20, 2026
b69c047
Fix arguments after Spring AI 2.0.0-M3 upgrade. Metrics. Add some @No…
labkey-adam Mar 21, 2026
6daabae
Treat McpException as guidance
labkey-adam Mar 21, 2026
ef65c92
Merge remote-tracking branch 'origin/develop' into fb_mcp_mvp
labkey-adam Mar 24, 2026
0dc76f7
Move McpServiceImpl to the Professional module
labkey-adam Mar 25, 2026
a9c1959
Better error handling
labkey-adam Mar 25, 2026
3adc8e1
Be more explicit about no container path
labkey-adam Mar 25, 2026
93e8575
Claude feedback
labkey-adam Mar 25, 2026
437abe6
Document search endpoints
labkey-adam Mar 25, 2026
2e38bd3
No leading slash
labkey-adam Mar 25, 2026
d378c1a
Tweaks
labkey-adam Mar 25, 2026
f21d9fd
Annotation-based permission checking for tools
labkey-adam Mar 25, 2026
dff04ac
Simple validation/guidance for missing parameters. Fix getSourceForSa…
labkey-adam Mar 25, 2026
f17141e
File-based module development guide
labkey-adam Mar 26, 2026
8fbc903
Send same message for non-existent and non-authorized container
labkey-adam Mar 26, 2026
1a05eeb
Validate all required parameters are provided before invoking tools
labkey-adam Mar 26, 2026
b50ea4d
Update message
labkey-adam Mar 26, 2026
db42f67
MCP developer guide
labkey-adam Mar 26, 2026
26f6c18
Merge remote-tracking branch 'origin/develop' into fb_mcp_mvp
labkey-adam Mar 26, 2026
78321e3
Merge remote-tracking branch 'origin/develop' into fb_mcp_mvp
labkey-adam Mar 30, 2026
b7cb058
Tweak content
labkey-adam Mar 30, 2026
00ab5af
No need for this check: framework handles missing parameters
labkey-adam Mar 30, 2026
c6338e1
Merge remote-tracking branch 'origin/develop' into fb_mcp_mvp
labkey-adam Mar 31, 2026
6a9bcee
Finish test
labkey-adam Apr 1, 2026
518071d
More targeted catch
labkey-adam Apr 1, 2026
e493fbb
Consistency
labkey-adam Apr 1, 2026
0d324b7
Switch to schemaName, tableName. New descriptions. Update test.
labkey-adam Apr 2, 2026
1d373b6
tableName -> queryName for consistency with other APIs
labkey-adam Apr 2, 2026
8f2a6c0
MCP server experimental feature
labkey-adam Apr 2, 2026
0a8758b
Merge remote-tracking branch 'origin/develop' into fb_mcp_mvp
labkey-adam Apr 2, 2026
f55386d
Merge remote-tracking branch 'origin/develop' into fb_mcp_mvp
labkey-adam Apr 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
11 changes: 11 additions & 0 deletions api/src/org/labkey/api/mcp/McpException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.labkey.api.mcp;

// A special exception that MCP endpoints can throw when they want to provide guidance to the client without making
// it a big red error. The message will be extracted and sent as text to the client.
public class McpException extends RuntimeException
{
public McpException(String message)
{
super(message);
}
}
23 changes: 21 additions & 2 deletions api/src/org/labkey/api/mcp/McpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
import jakarta.servlet.http.HttpSession;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.NonNull;
import org.labkey.api.data.Container;
import org.labkey.api.module.McpProvider;
import org.labkey.api.security.User;
import org.labkey.api.services.ServiceRegistry;
import org.labkey.api.util.HtmlString;
import org.springaicommunity.mcp.provider.resource.SyncMcpResourceProvider;
import org.labkey.api.writer.ContainerUser;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.mcp.annotation.provider.resource.SyncMcpResourceProvider;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
Expand All @@ -31,7 +35,22 @@
public interface McpService extends ToolCallbackProvider
{
// marker interface for classes that we will "ingest" using Spring annotations
interface McpImpl {}
interface McpImpl
{
default ContainerUser getContext(ToolContext toolContext)
{
User user = (User)toolContext.getContext().get("user");
Container container = (Container)toolContext.getContext().get("container");
if (container == null)
throw new McpException("You need to set a container path before invoking this tool");
return ContainerUser.create(container, user);
}

default User getUser(ToolContext toolContext)
{
return (User)toolContext.getContext().get("user");
}
}

static @NotNull McpService get()
{
Expand Down
41 changes: 34 additions & 7 deletions core/src/org/labkey/core/CoreMcp.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
package org.labkey.core;

import org.json.JSONObject;
import org.labkey.api.collections.LabKeyCollectors;
import org.labkey.api.data.Container;
import org.labkey.api.mcp.McpContext;
import org.labkey.api.data.ContainerManager;
import org.labkey.api.mcp.McpService;
import org.labkey.api.security.User;
import org.labkey.api.security.permissions.ReadPermission;
import org.labkey.api.settings.AppProps;
import org.labkey.api.settings.LookAndFeelProperties;
import org.labkey.api.study.Study;
import org.labkey.api.study.StudyService;
import org.labkey.api.util.HtmlString;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;

import java.util.Map;
import java.util.Objects;

import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.labkey.core.mcp.McpServiceImpl.PATH_CACHE;

public class CoreMcp implements McpService.McpImpl
{
// TODO ChatSessions are currently per session. The McpService should detect change of folder.
@Tool(description = "Call this tool before answering any prompts! This tool provides useful context information about the current user (name, userid), webserver (name, url, description), and current folder (name, path, url, description).")
String whereAmIWhoAmITalkingTo()
@Tool(description = "Call this tool before answering any prompts! This tool provides useful context information about the current user (name, userid), webserver (name, url, description), and current folder (name, path, url, description).")
String whereAmIWhoAmITalkingTo(ToolContext context)
{
McpContext context = McpContext.get();
User user = context.getUser();
Container folder = context.getContainer();
var cu = getContext(context);
User user = cu.getUser();
Container folder = cu.getContainer();
AppProps appProps = AppProps.getInstance();
Study study = null != StudyService.get() ? Objects.requireNonNull(StudyService.get()).getStudy(folder) : null;
LookAndFeelProperties laf = LookAndFeelProperties.getInstance(folder);
Expand Down Expand Up @@ -63,4 +67,27 @@ String whereAmIWhoAmITalkingTo()
"site", siteObj
)).toString();
}

@Tool(description = "List the hierarchical path for every container in the server where the user has read permissions.")
String listContainers(ToolContext toolContext)
{
return ContainerManager.getAllChildren(ContainerManager.getRoot(), getUser(toolContext), ReadPermission.class)
.stream()
.map(Container::getPath)
.collect(LabKeyCollectors.toJSONArray())
.toString();
}

@Tool(description = "Every tool in this MCP requires a container path, e.g. /MyProject/MyFolder. A container is also called a folder or project. Please prompt the user for a container path and use this tool to save the path for this session.")
String setContainer(ToolContext context, @ToolParam(description = "Container path, e.g. /MyProject/MyFolder", required = true) String containerPath)
{
Container container = ContainerManager.getForPath(containerPath);
if (container != null)
{
PATH_CACHE.put((String) context.getContext().get("sessionId"), containerPath);
return "OK!";
}

return "That's not a valid container path. Try using listContainers to see them.";
}
}
Loading
Loading