-
Notifications
You must be signed in to change notification settings - Fork 7
McpService #7329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
labkey-matthewb
wants to merge
41
commits into
develop
Choose a base branch
from
fb_sql_experiment
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
McpService #7329
Changes from all commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
1a3b9a2
sql experiment
labkey-matthewb deb64b1
pk/fk
labkey-matthewb ac07378
LabKeySql.md
labkey-matthewb 67d21fb
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb 1be5fd7
comment out console.log
labkey-matthewb 61746b2
include saved queries
labkey-matthewb 5dfe50c
Nested Schema
labkey-matthewb 9351637
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb a6a383d
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb 372671b
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb ccc467e
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb 36ecdf7
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb c1f6a01
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb 744459f
McpService checkpoint
labkey-matthewb b650056
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb 099b3bd
AbstractAgentAction
labkey-matthewb 78b2fc0
migration to spring-ai for chat client
labkey-matthewb 05b8b3b
Merge branch 'develop' into fb_sql_experiment
labkey-jeckels 5575dec
New files
labkey-matthewb d21d6d1
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb efd70b6
Merge remote-tracking branch 'origin/fb_sql_experiment' into fb_sql_e…
labkey-jeckels 028df68
Move methods from querycontroller.java -> querymcp.java
labkey-matthewb db36682
Merge remote-tracking branch 'origin/fb_sql_experiment' into fb_sql_e…
labkey-jeckels c12bbea
Merge branch 'develop' into fb_sql_experiment
labkey-jeckels 657dd27
McpService.sendMessageEx()
labkey-matthewb d54c910
delete commented code
labkey-matthewb c1b9ff5
McpService.getVectorStore()
labkey-matthewb 1fd3ca7
delete commented code
labkey-matthewb 4c00c82
springAiVersion=1.1.2
labkey-matthewb 71c3323
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb 4dc4014
QueryMCP.saveColumnDescription()
labkey-matthewb 59a969b
isChatReady
labkey-matthewb f0efa3d
SearchMCP
labkey-matthewb 7d166aa
SearchMCP, CoreMcp
labkey-matthewb 687381d
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb 17f0407
<% if (isChatReady) { %>
labkey-matthewb 837e1ab
comment
labkey-matthewb 9c1554c
Merge remote-tracking branch 'origin/develop' into fb_sql_experiment
labkey-matthewb 8ac79eb
remove unused dependency (moved to api)
labkey-matthewb 7669e93
CR cleanup
labkey-matthewb a7ce75f
comments
labkey-matthewb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package org.labkey.api.mcp; | ||
|
|
||
| import com.google.genai.errors.ClientException; | ||
| import com.google.genai.errors.ServerException; | ||
| import jakarta.servlet.http.HttpSession; | ||
| import org.json.JSONObject; | ||
| import org.labkey.api.action.ReadOnlyApiAction; | ||
| import org.labkey.api.util.HtmlString; | ||
| import org.springframework.ai.chat.client.ChatClient; | ||
| import org.springframework.validation.BindException; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| import static org.apache.commons.lang3.StringUtils.isNotBlank; | ||
|
|
||
| /** | ||
| * "agent" it is too strong a word, but if you want to create a tools specific chat endpoint then | ||
| * start here. | ||
| * First implement getServicePrompt() to tell your "agent its mission. You can also listen in on the | ||
| * conversation to help you user get the right results. | ||
| */ | ||
| public abstract class AbstractAgentAction<F extends PromptForm> extends ReadOnlyApiAction<F> | ||
| { | ||
| protected abstract String getAgentName(); | ||
|
|
||
| protected abstract String getServicePrompt(); | ||
|
|
||
| protected ChatClient getChat() | ||
| { | ||
| HttpSession session = getViewContext().getRequest().getSession(true); | ||
| ChatClient chatSession = McpService.get().getChat(session, getAgentName(), this::getServicePrompt); | ||
| return chatSession; | ||
| } | ||
|
|
||
| @Override | ||
| public Object execute(PromptForm form, BindException errors) throws Exception | ||
| { | ||
| try (var mcpPush = McpContext.withContext(getViewContext())) | ||
| { | ||
| ChatClient chatSession = getChat(); | ||
| if (null == chatSession) | ||
| return new JSONObject(Map.of( | ||
| "contentType", "text/plain", | ||
| "response", "Service is not ready yet", | ||
| "success", Boolean.FALSE)); | ||
|
|
||
| String prompt = form.getPrompt(); | ||
| McpService.MessageResponse response = McpService.get().sendMessage(chatSession, prompt); | ||
| var ret = new JSONObject(Map.of("success", Boolean.TRUE)); | ||
| if (!HtmlString.isBlank(response.html())) | ||
| { | ||
| ret.put("contentType", "text/html"); | ||
| ret.put("response", response.html()); | ||
| } | ||
| else if (isNotBlank(response.text())) | ||
| { | ||
| ret.put("contentType", response.contentType()); | ||
| ret.put("response", response.text()); | ||
| } | ||
| else | ||
| { | ||
| ret.put("contentType", "text/plain"); | ||
| ret.put("response", "I got nothing"); | ||
| } | ||
| return ret; | ||
| } | ||
| catch (ServerException x) | ||
| { | ||
| return new JSONObject(Map.of( | ||
| "error", x.getMessage(), | ||
| "text", "ERROR: " + x.getMessage(), | ||
| "success", Boolean.FALSE)); | ||
| } | ||
| catch (ClientException ex) | ||
| { | ||
| var ret = new JSONObject(Map.of( | ||
| "text", ex.getMessage(), | ||
| "user", getViewContext().getUser().getName(), | ||
| "success", Boolean.FALSE)); | ||
| return ret; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| package org.labkey.api.mcp; | ||
|
|
||
| import org.jetbrains.annotations.NotNull; | ||
| import org.labkey.api.data.Container; | ||
| import org.labkey.api.security.User; | ||
| import org.labkey.api.security.permissions.ReadPermission; | ||
| import org.labkey.api.view.UnauthorizedException; | ||
| import org.labkey.api.writer.ContainerUser; | ||
| import org.springframework.ai.chat.model.ToolContext; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * TODO MCP tool calling supports passing along a ToolContext. And most all | ||
| * interesting tools probably need a User and Container. This is not all hooked-up | ||
| * yet. This is an area for further investiation. | ||
| */ | ||
| public class McpContext implements ContainerUser | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A sentence or two of docs would be helpful here too. |
||
| { | ||
| final User user; | ||
| final Container container; | ||
|
|
||
| public McpContext(ContainerUser ctx) | ||
| { | ||
| this.container = ctx.getContainer(); | ||
| this.user = ctx.getUser(); | ||
| } | ||
|
|
||
| public McpContext(Container container, User user) | ||
| { | ||
| if (!container.hasPermission(user, ReadPermission.class)) | ||
| throw new UnauthorizedException(); | ||
| this.container = container; | ||
| this.user = user; | ||
| } | ||
|
|
||
| public ToolContext getToolContext() | ||
| { | ||
| return new ToolContext(Map.of("container", getContainer(), "user", getUser())); | ||
| } | ||
|
|
||
|
|
||
| @Override | ||
| public Container getContainer() | ||
| { | ||
| return container; | ||
| } | ||
|
|
||
| @Override | ||
| public User getUser() | ||
| { | ||
| return user; | ||
| } | ||
|
|
||
|
|
||
| // | ||
| // I'd like to get away from using ThreadLocal, but I haven't | ||
| // researched if there are other ways to pass context around to Tools registerd by McpService | ||
| // | ||
|
|
||
| private static final ThreadLocal<McpContext> contexts = new ThreadLocal(); | ||
|
|
||
| public static @NotNull McpContext get() | ||
| { | ||
| var ret = contexts.get(); | ||
| if (null == ret) | ||
| throw new IllegalStateException("McpContext is not set"); | ||
| return ret; | ||
| } | ||
|
|
||
| public static AutoCloseable withContext(ContainerUser ctx) | ||
| { | ||
| return with(new McpContext(ctx)); | ||
| } | ||
|
|
||
| public static AutoCloseable withContext(Container container, User user) | ||
| { | ||
| return with(new McpContext(container, user)); | ||
| } | ||
|
|
||
| private static AutoCloseable with(McpContext ctx) | ||
| { | ||
| final McpContext prev = contexts.get(); | ||
| contexts.set(ctx); | ||
| return () -> contexts.set(prev); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package org.labkey.api.mcp; | ||
|
|
||
|
|
||
| import io.modelcontextprotocol.server.McpServerFeatures; | ||
| import jakarta.servlet.http.HttpSession; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jspecify.annotations.NonNull; | ||
| import org.labkey.api.module.McpProvider; | ||
| import org.labkey.api.services.ServiceRegistry; | ||
| import org.labkey.api.util.HtmlString; | ||
| import org.springaicommunity.mcp.provider.resource.SyncMcpResourceProvider; | ||
| import org.springframework.ai.chat.client.ChatClient; | ||
| import org.springframework.ai.support.ToolCallbacks; | ||
| import org.springframework.ai.tool.ToolCallback; | ||
| import org.springframework.ai.tool.ToolCallbackProvider; | ||
| import org.springframework.ai.vectorstore.VectorStore; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.function.Supplier; | ||
|
|
||
| /** | ||
| * This service lets you expose functionality over the MCP protocol (only simple http for now). This allows | ||
| * external chat sessions to pull information from LabKey Server. These methods are also made available | ||
| * to chat session shosted by LabKey (see AbstractAgentAction). | ||
| * <p></p> | ||
| * These calls are not security checked. Any tools registered here must check user permissions. Maybe that | ||
| * will come as we get further along. Note that the LLM may make callbacks concerning containers other than the | ||
| * current container. This is an area for investigation. | ||
| */ | ||
| public interface McpService extends ToolCallbackProvider | ||
| { | ||
| // marker interface for classes that we will "ingest" using Spring annotations | ||
| interface McpImpl {}; | ||
|
|
||
| static @NotNull McpService get() | ||
| { | ||
| return ServiceRegistry.get().getService(McpService.class); | ||
| } | ||
|
|
||
| static void setInstance(McpService service) | ||
| { | ||
| ServiceRegistry.get().registerService(McpService.class, service); | ||
| } | ||
|
|
||
| boolean isReady(); | ||
|
|
||
|
|
||
| default void register(McpImpl obj) | ||
| { | ||
| ToolCallback[] tools = ToolCallbacks.from(obj); | ||
| if (null != tools && tools.length > 0) | ||
| registerTools(Arrays.asList(tools)); | ||
|
|
||
| List<McpServerFeatures.SyncResourceSpecification> ret; | ||
| var resources = new SyncMcpResourceProvider(List.of(obj)).getResourceSpecifications(); | ||
| if (null != resources && !resources.isEmpty()) | ||
| registerResources(resources); | ||
| } | ||
|
|
||
|
|
||
| default void register(McpProvider mcp) | ||
| { | ||
| registerTools(mcp.getMcpTools()); | ||
| registerPrompts(mcp.getMcpPrompts()); | ||
| registerResources(mcp.getMcpResources()); | ||
| } | ||
|
|
||
| void registerTools(@NotNull List<ToolCallback> tools); | ||
|
|
||
| void registerPrompts(@NotNull List<McpServerFeatures.SyncPromptSpecification> prompts); | ||
|
|
||
| void registerResources(@NotNull List<McpServerFeatures.SyncResourceSpecification> resources); | ||
|
|
||
| @Override | ||
| ToolCallback @NonNull [] getToolCallbacks(); | ||
|
|
||
| ChatClient getChat(HttpSession session, String agentName, Supplier<String> systemPromptSupplier); | ||
|
|
||
| record MessageResponse(String contentType, String text, HtmlString html) {} | ||
|
|
||
| /** get consolidated response (good for many text oriented agents/use-cases) */ | ||
| MessageResponse sendMessage(ChatClient chat, String message); | ||
|
|
||
| /** get individual response parts, useful for agents that generate SQL or programmatic responses */ | ||
| default List<MessageResponse> sendMessageEx(ChatClient chat, String message) | ||
| { | ||
| return List.of(sendMessage(chat, message)); | ||
| } | ||
|
|
||
| /** | ||
| * return an in-memory Vector store for prototyping RAG features | ||
| * CONSIDER: Is it possible to implement VectorStoreRetriever wrapper for SearchService??? | ||
| */ | ||
| VectorStore getVectorStore(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package org.labkey.api.mcp; | ||
|
|
||
| public class PromptForm | ||
| { | ||
| public String prompt; | ||
|
|
||
| public void setPrompt(String prompt) | ||
| { | ||
| this.prompt = prompt; | ||
| } | ||
|
|
||
| public String getPrompt() | ||
| { | ||
| return this.prompt; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package org.labkey.api.module; | ||
|
|
||
| import io.modelcontextprotocol.server.McpServerFeatures; | ||
| import org.springframework.ai.tool.ToolCallback; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface McpProvider | ||
| { | ||
| default List<ToolCallback> getMcpTools() | ||
| { | ||
| return List.of(); | ||
| } | ||
|
|
||
| default List<McpServerFeatures.SyncPromptSpecification> getMcpPrompts() | ||
| { | ||
| return List.of(); | ||
| } | ||
|
|
||
| default List<McpServerFeatures.SyncResourceSpecification> getMcpResources() | ||
| { | ||
| return List.of(); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.