Skip to content

Commit 5400bf8

Browse files
kroepkemonraxrobfromboulderbernd
authored
Add MCP server support to Graylog (#23462)
* initial, manual, mcp server support * suppress audit events for testing and add changelog * made Tool pluggable the mcp service now uses a tool map (name to instance) to declare and call tools the implemenation of list_streams should be replaced with resources (currently stubbed but not implemented) basic support for json schema declaration is available but untested * add missing license header * add resources and generate jsonschema from input parameters * add missing license headers * fix: replace Nullable annotation import * check MCP-Protocol-Version header * fix: resources/read method implementation * add dashboard and eventdefinitions as resources * fix: wrong logic in MCP-Protocol-Version check * WIP: resources-as-tools * fix: use the right access modifier for params * chore: specify locale * feat(ListResourceTool): add support for pagination * update list_resource tool description * add systeminfo tool * chore(ListStreamsTool): use the same format as overlord * add list_inputs tool * add list_indices tool * use the same formats as overlord * add list_index_sets tool * add search tool * add missing Locale * fix: use IndexSetResponse instead of IndexSetSummary * support prompts/list to make claude happy, it calls it even though we don't advertise supporting it. add some logging * add prompt support and mcp proxy to use with claude desktop * add getUser method to permission helper * add markdown builder * support for structured content responses * remove unnecessary test * fix missing license headers * remove unused helper method * support listing resource templates * add kv md item from builder-like objects * emit audit events for mcp protocol messages * add audit events to remaining mcp protocol handlers * revise MarkdownBuilder * added role and permission for MCP access users now need to have the mcp_server:access permission to be able to access the mcp endpoint all other permission checks remain as always, this is just to selectively allow access to mcp in graylog renamed McpResource to McpRestResource as the name was a bit ambiguous * cannot have a public constant, make it private and add comment * pull up PermissionHelper and hand it the current user instead of reloading it everywhere added better exception handling to produce the correct audit event * add test for McpService * add SystemInfoFormattedTool for output formatting comparisons * add MarkdownBuilderTest * Add MCP configuration panel and storage * remove exception from signatures * hook up cluster config to allow disabling MCP access entirely add minimal test (injection is a pain unfortunately) fix in memory cluster config service to work with AutoValue classes * Remove disclaimers * Remove unused import * add jakarta jsonschema module and include default values in tool schema * include searchuser from resource context and migrate search tool to scripting api * convert to junit5 * simplify test, for some unknown reason the injector fails on CI * remove unused subject in scripting api signatures, add field list to tool, refactor to fit test * add aggregate_messages tool * adjust jsonschema generator to avoid creating () suffix for field-derived properties * add list_fields tool * add response trace log for debugging purposes * make schema generators customizable from plugins * Add tool for getting current time * honor @jsonvalue annotations some enums have a value annotation to serialize as lower-case not looking at this when creating the output schema leads to validation failures with strict clients fix #23880 * remove static prompt, we don't have a good mechanism to ship these for now * fix prompt call tests * add permission checks to ResourceProviders, also used by the corresponding tools for resources * fix inverted permission check * move permission check to viewService call * lower log levels and remove mcp library logger config * remove mcp proxy implementation, there are better prebuilt versions out there * move mcp related dependencies into top level pom * address review comments for mcp rest resource * replace NotFoundException in resources with Optional return value, adapt call sites and tests * address review comments, remove hardcoded product name, remove commented code * Set enable_remote_access to false by default * Switch log statement in McpService to DEBUG * Remove traces of org.graylog2.database.NotFoundException Especially from the ResourceProvider interface Javadoc. * Remove product name from DashboardResourceProvider * Use content-type constants in McpRestResource * Use CustomizationConfig#productName instead of hardcoding "Graylog" * Inject GRNRegistry instead of using a static creator Otherwise, we will miss some types from plugins. * Cleanup variable definition in McpService * Remove commented code from PaginatedList * More product name removal * Remove product name from and cleanup ListIndexSetsTool * Add missing permission check to and cleanup ListIndicesTool * Fix whitespace in output * Remove product name and mention of pagination * Remove product name and commented code from ListStreamsTool * Fix warnings in PermissionHelper * Remove product name from ReadResourceTool * simplify tool, pagination isn't supported, so we take it out. use explicit return records instead of paginated list of lists * Remove product name from SearchMessagesTool and document limit parameter * Use customized product name instead of hardcoded "Graylog" Bind SystemInfoFormattedTool. * remove paginatedlist, we can't reasonably support pagination right now * Get cluster ID from ServerStatus * Return a result if tool call failed in McpService * Adjust McpConfig.tsx - Remove telemetry comment as we apparently don't need it - Use "MCP Server" for clarification - Remove obsolete permission comment * Re-add customized product name to doc strings * Fix McpServiceTest * be extra cautious and add markdown escaping in case this ever gets presented with user input * Fix string format argument in McpService to return error message * Add Javadoc to CurrentTimeTool --------- Co-authored-by: Ramon Marquez <[email protected]> Co-authored-by: robfromboulder <[email protected]> Co-authored-by: Bernd Ahlers <[email protected]> Co-authored-by: Bernd Ahlers <[email protected]>
1 parent ac3f307 commit 5400bf8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4840
-17
lines changed

changelog/unreleased/pr-23462.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type = "a"
2+
message = 'Added MCP server to Graylog'
3+
4+
issues = []
5+
pulls = ["23462"]

graylog2-server/pom.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,22 @@
797797
<groupId>io.grpc</groupId>
798798
<artifactId>grpc-services</artifactId>
799799
</dependency>
800+
<dependency>
801+
<groupId>io.modelcontextprotocol.sdk</groupId>
802+
<artifactId>mcp</artifactId>
803+
</dependency>
804+
<dependency>
805+
<groupId>com.github.victools</groupId>
806+
<artifactId>jsonschema-generator</artifactId>
807+
</dependency>
808+
<dependency>
809+
<groupId>com.github.victools</groupId>
810+
<artifactId>jsonschema-module-jackson</artifactId>
811+
</dependency>
812+
<dependency>
813+
<groupId>com.github.victools</groupId>
814+
<artifactId>jsonschema-module-jakarta-validation</artifactId>
815+
</dependency>
800816

801817
<!-- our basic test libraries, please only add infrastructure dependencies here -->
802818
<dependency>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.jsonschema;
18+
19+
import com.fasterxml.jackson.databind.node.ObjectNode;
20+
import com.github.victools.jsonschema.generator.Module;
21+
import com.github.victools.jsonschema.generator.SchemaGenerationContext;
22+
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
23+
import com.github.victools.jsonschema.generator.SchemaKeyword;
24+
import com.github.victools.jsonschema.generator.TypeAttributeOverrideV2;
25+
import com.github.victools.jsonschema.generator.TypeScope;
26+
27+
/**
28+
* Ensures that “empty” parameter classes yield:
29+
* {
30+
* "type": "object",
31+
* "properties": {}
32+
* }
33+
* instead of bare {} – which is non-compliant with the expected inputSchema/outputSchema according to
34+
* the <a href="https://modelcontextprotocol.io/specification/2025-06-18/schema#tool">2025-06-18 MCP Tool schema</a>
35+
*/
36+
public final class EmptyObjectAsObjectModule implements Module {
37+
38+
@Override
39+
public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
40+
builder.forTypesInGeneral().withTypeAttributeOverride(new ForceObjectTypeIfEmpty());
41+
}
42+
43+
private static final class ForceObjectTypeIfEmpty implements TypeAttributeOverrideV2 {
44+
@Override
45+
public void overrideTypeAttributes(ObjectNode schemaNode, TypeScope scope, SchemaGenerationContext context) {
46+
// If the generator didn’t decide on a type (and it’s not a $ref),
47+
// declare an object with an empty properties object.
48+
final String type = context.getKeyword(SchemaKeyword.TAG_TYPE);
49+
final String ref = context.getKeyword(SchemaKeyword.TAG_REF);
50+
final String props = context.getKeyword(SchemaKeyword.TAG_PROPERTIES);
51+
52+
if (!schemaNode.has(type) && !schemaNode.has(ref)) {
53+
schemaNode.put(type, "object");
54+
// create {} only if missing; won’t harm if later properties are added
55+
if (!schemaNode.has(props)) {
56+
schemaNode.putObject(props);
57+
}
58+
}
59+
}
60+
}
61+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.mcp.config;
18+
19+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
20+
import com.fasterxml.jackson.annotation.JsonCreator;
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import com.google.auto.value.AutoValue;
23+
24+
@JsonAutoDetect
25+
@AutoValue
26+
public abstract class McpConfiguration {
27+
public static final McpConfiguration DEFAULT_VALUES = create(false);
28+
29+
@JsonProperty("enable_remote_access")
30+
public abstract boolean enableRemoteAccess();
31+
32+
@JsonCreator
33+
public static McpConfiguration create(@JsonProperty("enable_remote_access") boolean enableRemoteAccess) {
34+
return new AutoValue_McpConfiguration(enableRemoteAccess);
35+
}
36+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.mcp.resources;
18+
19+
import io.modelcontextprotocol.spec.McpSchema;
20+
import jakarta.inject.Inject;
21+
import jakarta.ws.rs.core.MediaType;
22+
import org.glassfish.jersey.uri.UriTemplate;
23+
import org.graylog.grn.GRN;
24+
import org.graylog.grn.GRNRegistry;
25+
import org.graylog.grn.GRNType;
26+
import org.graylog.grn.GRNTypes;
27+
import org.graylog.mcp.server.ResourceProvider;
28+
import org.graylog.mcp.tools.PermissionHelper;
29+
import org.graylog.plugins.views.search.views.ViewDTO;
30+
import org.graylog.plugins.views.search.views.ViewService;
31+
import org.graylog2.rest.models.SortOrder;
32+
import org.graylog2.search.SearchQuery;
33+
import org.graylog2.web.customization.CustomizationConfig;
34+
35+
import java.net.URI;
36+
import java.util.List;
37+
import java.util.Optional;
38+
import java.util.stream.Stream;
39+
40+
import static org.graylog2.shared.utilities.StringUtils.f;
41+
42+
public class DashboardResourceProvider extends ResourceProvider {
43+
public static final GRNType GRN_TYPE = GRNTypes.DASHBOARD;
44+
private static final String GRN_TEMPLATE = GRN_TYPE.toGRN("{dashboard_id}").toString();
45+
46+
private final ViewService viewService;
47+
private final GRNRegistry grnRegistry;
48+
private final String productName;
49+
50+
@Inject
51+
public DashboardResourceProvider(ViewService viewService,
52+
CustomizationConfig customizationConfig,
53+
GRNRegistry grnRegistry) {
54+
this.viewService = viewService;
55+
this.grnRegistry = grnRegistry;
56+
this.productName = customizationConfig.productName();
57+
}
58+
59+
@Override
60+
public Template resourceTemplate() {
61+
return new Template(
62+
new UriTemplate(GRN_TEMPLATE),
63+
"Dashboards",
64+
"Dashboards",
65+
f("Access dashboards in this %s cluster", productName),
66+
MediaType.APPLICATION_JSON
67+
);
68+
}
69+
70+
@Override
71+
public Optional<McpSchema.Resource> read(final PermissionHelper permissionHelper, URI uri) {
72+
final GRN grn = grnRegistry.parse(uri.toString());
73+
if (!grn.isType(GRNTypes.DASHBOARD)) {
74+
throw new IllegalArgumentException("Invalid GRN URI, expected a Dashboard GRN: " + uri);
75+
}
76+
final Optional<ViewDTO> viewDTO = viewService.get(grn.entity());
77+
if (viewDTO.isEmpty()) {
78+
return Optional.empty();
79+
}
80+
final var dashboard = viewDTO.get();
81+
if (!permissionHelper.getSearchUser().canReadView(dashboard)) {
82+
return Optional.empty();
83+
}
84+
return Optional.of(McpSchema.Resource.builder()
85+
.name(dashboard.title())
86+
.description(dashboard.description())
87+
.uri(grn.toString())
88+
.build());
89+
}
90+
91+
@Override
92+
public List<McpSchema.Resource> list(final PermissionHelper permissionHelper) {
93+
final Stream<ViewDTO> resultStream = viewService.searchPaginatedByType(
94+
ViewDTO.Type.DASHBOARD,
95+
new SearchQuery(""),
96+
dashboard -> permissionHelper.getSearchUser().canReadView(dashboard),
97+
SortOrder.ASCENDING, ViewDTO.FIELD_ID, 1, 0).stream();
98+
99+
try (resultStream) {
100+
return resultStream
101+
.map(dashboard -> new McpSchema.Resource(
102+
GRN_TYPE.toGRN(dashboard.id()).toString(),
103+
dashboard.title(),
104+
dashboard.title(),
105+
dashboard.description(),
106+
null,
107+
null,
108+
null,
109+
null))
110+
.toList();
111+
}
112+
}
113+
114+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog.mcp.resources;
18+
19+
import io.modelcontextprotocol.spec.McpSchema;
20+
import jakarta.inject.Inject;
21+
import jakarta.ws.rs.core.MediaType;
22+
import org.glassfish.jersey.uri.UriTemplate;
23+
import org.graylog.events.processor.DBEventDefinitionService;
24+
import org.graylog.events.processor.EventDefinitionDto;
25+
import org.graylog.grn.GRN;
26+
import org.graylog.grn.GRNRegistry;
27+
import org.graylog.grn.GRNType;
28+
import org.graylog.grn.GRNTypes;
29+
import org.graylog.mcp.server.ResourceProvider;
30+
import org.graylog.mcp.tools.PermissionHelper;
31+
import org.graylog2.shared.security.RestPermissions;
32+
import org.graylog2.web.customization.CustomizationConfig;
33+
34+
import java.net.URI;
35+
import java.util.List;
36+
import java.util.Optional;
37+
38+
import static org.graylog2.shared.utilities.StringUtils.f;
39+
40+
public class EventDefinitionResourceProvider extends ResourceProvider {
41+
public static final GRNType GRN_TYPE = GRNTypes.EVENT_DEFINITION;
42+
private static final String GRN_TEMPLATE = GRN_TYPE.toGRN("{event_definition_id}").toString();
43+
44+
private final DBEventDefinitionService eventDefinitionService;
45+
private final GRNRegistry grnRegistry;
46+
private final String productName;
47+
48+
@Inject
49+
public EventDefinitionResourceProvider(DBEventDefinitionService eventDefinitionService,
50+
CustomizationConfig customizationConfig,
51+
GRNRegistry grnRegistry) {
52+
this.eventDefinitionService = eventDefinitionService;
53+
this.grnRegistry = grnRegistry;
54+
this.productName = customizationConfig.productName();
55+
}
56+
57+
@Override
58+
public Template resourceTemplate() {
59+
return new Template(
60+
new UriTemplate(GRN_TEMPLATE),
61+
"EventDefinition",
62+
"EventDefinition",
63+
f("Access Event Definitions in this %s cluster", productName),
64+
MediaType.APPLICATION_JSON
65+
);
66+
}
67+
68+
@Override
69+
public Optional<McpSchema.Resource> read(final PermissionHelper permissionHelper, URI uri) {
70+
final GRN grn = grnRegistry.parse(uri.toString());
71+
if (!grn.isType(GRNTypes.EVENT_DEFINITION)) {
72+
throw new IllegalArgumentException("Invalid GRN URI, expected an Event Definition GRN: " + uri);
73+
}
74+
if (!permissionHelper.isPermitted(RestPermissions.EVENT_DEFINITIONS_READ, grn.entity())) {
75+
return Optional.empty();
76+
}
77+
final Optional<EventDefinitionDto> definitionDto = eventDefinitionService.get(grn.entity());
78+
if (definitionDto.isEmpty()) {
79+
return Optional.empty();
80+
}
81+
final var eventDefinition = definitionDto.get();
82+
return Optional.of(McpSchema.Resource.builder()
83+
.name(eventDefinition.title())
84+
.description(eventDefinition.description())
85+
.uri(grn.toString())
86+
.build());
87+
}
88+
89+
@Override
90+
public List<McpSchema.Resource> list(final PermissionHelper permissionHelper) {
91+
try (var dtos = eventDefinitionService.streamAll()) {
92+
return dtos
93+
.filter(eventDefinition -> permissionHelper.isPermitted(RestPermissions.EVENT_DEFINITIONS_READ,
94+
eventDefinition.id()))
95+
.map(eventDefinition -> new McpSchema.Resource(
96+
GRN_TYPE.toGRN(eventDefinition.id()).toString(),
97+
eventDefinition.title(),
98+
eventDefinition.title(),
99+
eventDefinition.description(),
100+
null,
101+
null,
102+
null,
103+
null))
104+
.toList();
105+
}
106+
}
107+
108+
}

0 commit comments

Comments
 (0)