Class McpServerToolService

java.lang.Object
io.meeds.mcp.server.service.McpServerToolService

@Service public class McpServerToolService extends Object
  • Field Details

    • SCOPE_PATTERN

      public static final String SCOPE_PATTERN
      See Also:
    • READ_SCOPE_AUTHORITY

      public static final String READ_SCOPE_AUTHORITY
    • WRITE_SCOPE_AUTHORITY

      public static final String WRITE_SCOPE_AUTHORITY
    • WRITE_APPROVE_SCOPE_AUTHORITY

      public static final String WRITE_APPROVE_SCOPE_AUTHORITY
  • Constructor Details

    • McpServerToolService

      public McpServerToolService()
  • Method Details

    • getToolDefinitionByMethodName

      public SimpleToolDefinition getToolDefinitionByMethodName(String methodName)
      Parameters:
      methodName - the Java method name implementing the tool
      Returns:
      the tool definition registered under that method's snake-case name, or null when no such tool exists
    • getToolDefinition

      public SimpleToolDefinition getToolDefinition(String toolName)
      Parameters:
      toolName - the MCP tool name
      Returns:
      the tool definition registered under that name, or null when no such tool exists
    • isRequireApproval

      public boolean isRequireApproval(String methodName, org.springframework.security.core.Authentication authentication)
      Tells whether a tool call must be confirmed by the end user before it runs. A tool marked as requiring approval only actually asks for one when the caller holds the approval scope.
      Parameters:
      methodName - the Java method name implementing the tool
      authentication - the current OAuth authentication
      Returns:
      true when the call must be approved by the user first
    • hasApprovalScope

      public boolean hasApprovalScope(org.springframework.security.core.Authentication authentication)
      Tells whether the caller's token carries the scope under which the user is asked to approve write tools.
      Parameters:
      authentication - the current OAuth authentication, may be null
      Returns:
      true when the token holds mcp.tools.writeWithApproval
    • isAlwaysAsk

      public boolean isAlwaysAsk(String toolName)
      Tells whether an administrator marked a tool "always ask": every call needs the user's approval on a card, no standing approval ever covers it, and a caller that can't be asked (a token without the approval scope, such as an external MCP client's) is refused.

      Read from the database on every call, never from a node-local memo nor from the setting cache, whose get answers null on a read failure exactly as for an absent flag. An absent flag means "not always ask"; a read that fails means "always ask": no standing approval applies to a tool whose policy can't be read.

      Parameters:
      toolName - the MCP tool name
      Returns:
      true when the tool is marked "always ask", or when its flag can't be read
    • getAlwaysAskToolNames

      public List<String> getAlwaysAskToolNames()
      Returns:
      the names of the tools marked "always ask", sorted
      Throws:
      IllegalStateException - when the flags can't be read
    • setAlwaysAsk

      public void setAlwaysAsk(String toolName, boolean alwaysAsk)
      Marks or unmarks a tool "always ask", see isAlwaysAsk(String).
      Parameters:
      toolName - the MCP tool name, which must exist
      alwaysAsk - the new flag
      Throws:
      IllegalArgumentException - when the tool doesn't exist
    • isAllowedTool

      public boolean isAllowedTool(String toolOrMethodName, org.springframework.security.core.Authentication authentication)
      Same check as isAllowedTool(SimpleToolDefinition, Authentication), for a caller holding only a name: an MCP tool name or the Java method name behind it, tried in that order.
      Parameters:
      toolOrMethodName - the MCP tool name or its Java method name
      authentication - the current OAuth authentication
      Returns:
      true when the caller may use the tool, false when the tool is unknown
    • isAllowedTool

      public boolean isAllowedTool(SimpleToolDefinition toolDefinition, org.springframework.security.core.Authentication authentication)
      Tells whether the authenticated caller may execute, or even see, one given tool: the MCP server must be globally on, and the token must carry a scope matching the tool's read or write nature.

      The per-user audience is deliberately not asked here. It is enforced at the door — McpServerOauthOpaqueTokenIntrospector refuses an out-of-audience subject — and only there, which is sufficient: one filter chain covers every path under /mcp, every bearer token is introspected on every request (nothing persists an authentication in the HTTP session, and the introspector does not cache), and every MCP verb — initialize, tools/list, tools/call, the SSE stream, DELETE — is a handler that runs after that chain. There is no in-JVM path to a tool callback that skips it, so a second audience check here could only repeat a verdict already given, and asking it per user had made the tools/list cache per user too.

      A null authentication is refused first: it is not the internal client, and the scope checks below dereference it.

      The internal client is exempt from the global flag, as it has always been: EVA calls its tools through the internal client-credentials grant, and that must keep working while MCP is globally off — the flag says no external client may connect, not that the in-product assistant loses its tools. The exemption is recognized by the OAuth client that owns the token (McpToolUtils.isInternalClientAuthentication(Authentication)) and not by the token subject, which is the user login on a user grant: a user whose login happened to equal the internal client id used to be exempt here, and is not any more.

      Parameters:
      toolDefinition - the tool being listed or called
      authentication - the current OAuth authentication, may be null
      Returns:
      true when the caller may use the tool
    • getToolDefinitions

      public Map<String,SimpleToolDefinition> getToolDefinitions()
      Returns:
      every known tool definition by name, importing them from the classpath on first access
    • updateToolDefinition

      public org.springframework.ai.tool.definition.ToolDefinition updateToolDefinition(String toolName, String title, String description, String inputSchema, boolean requireApproval, boolean disabled)
      Updates an administrable tool definition, persists the whole set and notifies listeners so that a running instance picks the change up without a restart.
      Parameters:
      toolName - the MCP tool name
      title - the new title
      description - the new description shown to the LLM
      inputSchema - the new JSON input schema
      requireApproval - whether calls must be approved by the end user
      disabled - whether the tool is withdrawn from the server
      Returns:
      the updated tool definition
    • addToolUpdateListener

      public void addToolUpdateListener(ToolListener listener)
      Registers a listener notified whenever a tool definition changes.
      Parameters:
      listener - the listener to add
    • isMcpServerEnabled

      public boolean isMcpServerEnabled()
      Returns:
      true when the MCP server is globally switched on. This is the instance-wide on/off flag only: it says nothing about whether a given user belongs to the MCP audience — ask isMcpServerEnabledForUser(String) for that.
    • isMcpServerEnabledForUser

      public boolean isMcpServerEnabledForUser(String username)
      Answers the per-user MCP question — is MCP globally on and is this user in its audience? — for the places that ask it: the door (McpServerOauthOpaqueTokenIntrospector) and the token endpoint (McpServerOAuthAccessTokenAudienceProvider). Both ask this one method so that they cannot drift apart.

      Both halves are asked of this service's own collaborators, deliberately not of ExoFeatureService.isFeatureActiveForUser. That API resolves the audience through whatever FeaturePlugin is registered under the feature name, and on a registry miss ExoFeatureServiceImpl falls back to the exo.feature.mcp.server.permissions property, which is unset on nearly every deployment and then reads as everybody. A miss would open the gate rather than close it, and an enforcement point must not fail open.

      No FeaturePlugin is registered for mcp.server at all: the one this addon once shipped answered a question nobody asked and was removed. The consequence to know is that a future caller of ExoFeatureService.isFeatureActiveForUser("mcp.server", user) would get that property fallback — everybody on a deployment that never set the property, whatever audience an administrator saved. Nothing calls it today; do not reintroduce it as a shortcut for this method.

      The global flag is read through ExoFeatureService.isActiveFeature and not through isMcpServerEnabled(), whose node-local memo would make the gate staler than the audience across a cluster. Neither half is memoised: the answer depends on the user and on an audience an administrator may change at any moment, and on a security input a stale wide answer is a hole rather than an inconvenience.

      Parameters:
      username - the platform login of the end user, may be null or blank
      Returns:
      true when that user may use the MCP server; false when the flag is off, when the user is outside the audience, or when no user was given
    • enableMcpServer

      public void enableMcpServer()
      Switches the MCP server on instance-wide and drops the memoised flag so the change is visible on the next request.
    • disableMcpServer

      public void disableMcpServer()
      Switches the MCP server off instance-wide and drops the memoised flag so the change is visible on the next request.