diff --git a/openig-core/src/main/java/org/forgerock/openig/alias/CoreClassAliasResolver.java b/openig-core/src/main/java/org/forgerock/openig/alias/CoreClassAliasResolver.java
index cb99023f1..6c72f135b 100644
--- a/openig-core/src/main/java/org/forgerock/openig/alias/CoreClassAliasResolver.java
+++ b/openig-core/src/main/java/org/forgerock/openig/alias/CoreClassAliasResolver.java
@@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2021-2026 3A Systems LLC.
*/
package org.forgerock.openig.alias;
@@ -61,6 +62,7 @@
import org.forgerock.openig.security.TrustManagerHeaplet;
import org.forgerock.openig.thread.ScheduledExecutorServiceHeaplet;
import org.openidentityplatform.openig.filter.ICAPFilter;
+import org.openidentityplatform.openig.filter.MCPServerFeaturesFilter;
import org.openidentityplatform.openig.mq.EmbeddedKafka;
import org.openidentityplatform.openig.mq.MQ_IBM;
import org.openidentityplatform.openig.mq.MQ_Kafka;
@@ -119,6 +121,7 @@ public class CoreClassAliasResolver implements ClassAliasResolver {
ALIASES.put("MQ_Kafka", MQ_Kafka.class);
ALIASES.put("MQ_IBM", MQ_IBM.class);
ALIASES.put("ICAP", ICAPFilter.class);
+ ALIASES.put("MCPServerFeaturesFilter", MCPServerFeaturesFilter.class);
}
@Override
diff --git a/openig-core/src/main/java/org/openidentityplatform/openig/filter/MCPServerFeaturesFilter.java b/openig-core/src/main/java/org/openidentityplatform/openig/filter/MCPServerFeaturesFilter.java
new file mode 100644
index 000000000..3e2be5c24
--- /dev/null
+++ b/openig-core/src/main/java/org/openidentityplatform/openig/filter/MCPServerFeaturesFilter.java
@@ -0,0 +1,329 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems LLC.
+ */
+
+package org.openidentityplatform.openig.filter;
+
+import org.forgerock.http.Filter;
+import org.forgerock.http.Handler;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.Status;
+import org.forgerock.json.JsonValue;
+import org.forgerock.openig.heap.GenericHeaplet;
+import org.forgerock.openig.heap.HeapException;
+import org.forgerock.services.context.Context;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.emptyList;
+import static org.forgerock.http.protocol.Response.newResponsePromise;
+import static org.forgerock.http.protocol.Responses.newInternalServerError;
+import static org.forgerock.json.JsonValue.field;
+import static org.forgerock.json.JsonValue.json;
+import static org.forgerock.json.JsonValue.object;
+
+/**
+ * MCPServerFeaturesFilter
+ *
+ * This filter enforces allow/deny policies for MCP (Management & Control Protocol)
+ * features exchanged as JSON-RPC payloads with an MCP server. It inspects both
+ * incoming requests and outgoing responses and removes or rejects features
+ * according to the configured rules.
+ *
+ *
Policy enforcement logic:
+ *
+ * - Deny lists take precedence over allow lists
+ * - Empty allow list means all features are allowed (unless denied)
+ * - Non-empty allow list means only listed features are allowed
+ * - Denied features are always blocked, regardless of allow list
+ *
+ *
+ *
+ * {
+ * "type": "MCPFeaturesFilter",
+ * "config": {
+ * "allow": {
+ * "tools": ["get_weather", "tool2"],
+ * "prompts": ["code_review", "prompt2"]
+ * },
+ * "deny": {
+ * "resources": ["file:///project/src/main.rs"],
+ * "resources/templates": ["file:///{path}"]
+ * }
+ * }
+ * }
+ *
+ */
+public class MCPServerFeaturesFilter implements Filter {
+
+ private static final Logger logger = LoggerFactory.getLogger(MCPServerFeaturesFilter.class);
+
+ Map> allowFeatures;
+ Map> denyFeatures;
+
+ public Map> getAllowFeatures() {
+ return allowFeatures;
+ }
+
+ public Map> getDenyFeatures() {
+ return denyFeatures;
+ }
+
+ @Override
+ public Promise filter(Context context, Request request, Handler next) {
+ JsonValue inputValue;
+ try {
+ inputValue = json(request.getEntity().getJson());
+ } catch (IOException e) {
+ logger.debug("Error parsing JSON request body", e);
+ return newResponsePromise(new Response(Status.BAD_REQUEST));
+ }
+ String method = inputValue.get("method").asString();
+
+ JsonValue methodNode = inputValue.get("method");
+ if (methodNode == null || methodNode.isNull()) {
+ logger.debug("Missing 'method' in JSON-RPC request");
+ return newResponsePromise(new Response(Status.BAD_REQUEST));
+ }
+
+ try {
+ checkFeaturesRequest(method, inputValue);
+ } catch (FeatureIsNotAllowedException e) {
+ logger.warn("feature {}: {} is not allowed", e.getMcpFeature(), e.getFeatureName());
+ Response response = getFeatureDeniedResponse(inputValue, e);
+ return newResponsePromise(response);
+ }
+
+ return next.handle(context, request)
+ .then(response -> {
+ JsonValue outputValue;
+ try {
+ outputValue = json(response.getEntity().getJson());
+ } catch (IOException e) {
+ logger.debug("Error parsing response JSON body", e);
+ return newInternalServerError();
+ }
+ JsonValue result = outputValue.get("result");
+ filterFeaturesResponse(method, result);
+ response.setEntity(outputValue);
+ return response;
+ });
+ }
+
+ private static Response getFeatureDeniedResponse(JsonValue inputValue, FeatureIsNotAllowedException e) {
+ String errMessage = "";
+ switch (e.getMcpFeature()){
+ case TOOLS:
+ errMessage = "Unknown tool: invalid_tool_name";
+ break;
+ case PROMPTS:
+ errMessage = "Unknown prompt: invalid_prompt_name";
+ break;
+ case RESOURCES:
+ errMessage = "Unknown resource: invalid_resource_name";
+ break;
+ case RESOURCES_TEMPLATES:
+ errMessage = "Unknown resource template: invalid_resource_template_name";
+ break;
+ }
+ JsonValue responseEntity = json(object(
+ field("jsonrpc", "2.0"),
+ field("id", inputValue.get("id")),
+ field("error", object(
+ field("code", -32602),
+ field("message", errMessage)
+ ))
+ ));
+ Response response = new Response(Status.OK);
+ response.setEntity(responseEntity);
+ return response;
+ }
+
+ private void checkFeaturesRequest(String method, JsonValue inputValue) throws FeatureIsNotAllowedException {
+ MCPFeature feature;
+ switch (method) {
+ case "tools/call":
+ feature = MCPFeature.TOOLS;
+ break;
+ case "prompts/get":
+ feature = MCPFeature.PROMPTS;
+ break;
+ case "resources/list":
+ feature = MCPFeature.RESOURCES;
+ break;
+ case "resources/templates/list":
+ feature = MCPFeature.RESOURCES_TEMPLATES;
+ break;
+ default:
+ return;
+ }
+
+ JsonValue queriedFeatureJson = inputValue.get("params").get(feature.idField);
+ if(queriedFeatureJson == null || queriedFeatureJson.isNull()) {
+ return;
+ }
+
+ String queriedFeatureName = queriedFeatureJson.asString();
+
+ if (!isFeatureAllowed(feature, queriedFeatureName)) {
+ throw new FeatureIsNotAllowedException(feature, queriedFeatureName);
+ }
+ }
+
+ private boolean isFeatureAllowed(MCPFeature feature, String featureName) {
+ List denied = this.denyFeatures.get(feature);
+ if (denied != null && !denied.isEmpty() && denied.contains(featureName)) {
+ return false;
+ }
+
+ List allowed = this.allowFeatures.get(feature);
+ if (allowed != null && !allowed.isEmpty()) {
+ return allowed.contains(featureName);
+ }
+ return true;
+ }
+
+ private void filterFeaturesResponse(String method, JsonValue result) {
+ MCPFeature feature;
+ switch (method) {
+ case "tools/list":
+ feature = MCPFeature.TOOLS;
+ break;
+ case "prompts/list":
+ feature = MCPFeature.PROMPTS;
+ break;
+ case "resources/list":
+ feature = MCPFeature.RESOURCES;
+ break;
+ case "resources/templates/list":
+ feature = MCPFeature.RESOURCES_TEMPLATES;
+ break;
+ default:
+ return;
+ }
+
+ List returnedFeatures = result.get(feature.name).asList()
+ .stream().map(JsonValue::json).collect(Collectors.toList());
+
+ List filteredReturnedFeatures
+ = filterResponseFeature(feature, returnedFeatures,
+ this.allowFeatures.get(feature), this.denyFeatures.get(feature));
+
+ result.put(feature.name, filteredReturnedFeatures);
+
+ }
+
+ /**
+ * Filter a list of feature objects.
+ *
+ * @param featuresList the original feature JSON objects
+ * @param allowed allowed names (empty == no allow constraint)
+ * @param denied denied names (empty == no deny constraint)
+ * @return filtered list (new list instance)
+ */
+ public List filterResponseFeature(MCPFeature mcpFeature,
+ List featuresList,
+ List allowed, List denied) {
+ List result = new ArrayList<>(featuresList);
+
+ if(denied != null && !denied.isEmpty()) {
+ result = result.stream()
+ .filter(t -> !denied.contains(t.get(mcpFeature.idField).asString()))
+ .collect(Collectors.toList());
+ }
+
+ if(allowed != null && !allowed.isEmpty()) {
+ result = featuresList.stream()
+ .filter(t -> allowed.contains(t.get(mcpFeature.idField).asString()))
+ .collect(Collectors.toList());
+ }
+
+
+
+ return result;
+ }
+
+ public static class Heaplet extends GenericHeaplet {
+
+ @Override
+ public Object create() throws HeapException {
+ MCPServerFeaturesFilter filter = new MCPServerFeaturesFilter();
+ JsonValue evaluatedConfig = config.as(evaluatedWithHeapProperties());
+ JsonValue allowConfig = evaluatedConfig.get("allow");
+ filter.allowFeatures = Arrays.stream(MCPFeature.values())
+ .collect(Collectors.toUnmodifiableMap(
+ f -> f,
+ f -> Collections.unmodifiableList(allowConfig.get(f.name)
+ .defaultTo(emptyList()).asList(String.class))
+ ));
+
+ JsonValue denyConfig = evaluatedConfig.get("deny");
+ filter.denyFeatures = Arrays.stream(MCPFeature.values())
+ .collect(Collectors.toUnmodifiableMap(
+ f -> f,
+ f -> Collections.unmodifiableList(denyConfig.get(f.name)
+ .defaultTo(emptyList()).asList(String.class))
+ ));
+ return filter;
+ }
+ }
+
+ public enum MCPFeature {
+ TOOLS("tools", "name"),
+ PROMPTS("prompts", "name"),
+ RESOURCES("resources", "uri"),
+ RESOURCES_TEMPLATES("resources/templates", "uriTemplate");
+
+ private final String name;
+
+ private final String idField;
+ MCPFeature(String name, String idField) {
+ this.name = name;
+ this.idField = idField;
+ }
+ }
+
+ static class FeatureIsNotAllowedException extends Exception {
+
+ private final String featureName;
+
+ private final MCPFeature mcpFeature;
+
+ public FeatureIsNotAllowedException(MCPFeature mcpFeature, String featureName) {
+ this.mcpFeature = mcpFeature;
+ this.featureName = featureName;
+ }
+
+ public MCPFeature getMcpFeature() {
+ return mcpFeature;
+ }
+
+ public String getFeatureName() {
+ return featureName;
+ }
+
+ }
+}
diff --git a/openig-core/src/test/java/org/openidentityplatform/openig/filter/MCPServerFeaturesFilterTest.java b/openig-core/src/test/java/org/openidentityplatform/openig/filter/MCPServerFeaturesFilterTest.java
new file mode 100644
index 000000000..10fe67613
--- /dev/null
+++ b/openig-core/src/test/java/org/openidentityplatform/openig/filter/MCPServerFeaturesFilterTest.java
@@ -0,0 +1,184 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems LLC.
+ */
+
+package org.openidentityplatform.openig.filter;
+
+import org.forgerock.http.Handler;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.Status;
+import org.forgerock.json.JsonValue;
+import org.forgerock.openig.heap.HeapException;
+import org.forgerock.openig.heap.HeapImpl;
+import org.forgerock.openig.heap.HeapUtilsTest;
+import org.forgerock.openig.heap.Name;
+import org.forgerock.services.context.Context;
+import org.forgerock.services.context.RootContext;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.json.JsonValue.array;
+import static org.forgerock.json.JsonValue.field;
+import static org.forgerock.json.JsonValue.json;
+import static org.forgerock.json.JsonValue.object;
+import static org.forgerock.openig.util.JsonValues.readJson;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertTrue;
+
+public class MCPServerFeaturesFilterTest {
+
+ private HeapImpl heap;
+ private JsonValue config;
+
+ @Mock
+ Handler testHandler;
+
+ private AutoCloseable closeable;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+
+ closeable = MockitoAnnotations.openMocks(this);
+
+ heap = HeapUtilsTest.buildDefaultHeap();
+
+ config = json(object());
+ config.put("allow", object(
+ field("tools", array("current_time_service")),
+ field("prompts", array("prompt1", "prompt2"))));
+
+ config.put("deny", object(
+ field("resources", array("res1")),
+ field("resources/templates", array("resource_template1"))));
+ }
+
+ @AfterMethod
+ public void tearDown() throws Exception {
+ closeable.close();
+ }
+
+
+ @Test
+ public void testFilterSetup() throws HeapException {
+
+ MCPServerFeaturesFilter filter = (MCPServerFeaturesFilter) new MCPServerFeaturesFilter
+ .Heaplet().create(Name.of("this"), config, heap);
+
+ assertThat(filter.getAllowFeatures()).isNotNull();
+ assertThat(filter.getDenyFeatures()).isNotNull();
+
+ assertThat(filter.getAllowFeatures().get(MCPServerFeaturesFilter.MCPFeature.TOOLS)).hasSize(1);
+ assertThat(filter.getAllowFeatures().get(MCPServerFeaturesFilter.MCPFeature.PROMPTS)).hasSize(2);
+ assertThat(filter.getDenyFeatures().get(MCPServerFeaturesFilter.MCPFeature.RESOURCES)).hasSize(1);
+ assertThat(filter.getDenyFeatures().get(MCPServerFeaturesFilter.MCPFeature.RESOURCES_TEMPLATES)).hasSize(1);
+ }
+
+ @Test
+ public void testToolsListRestriction() throws Exception {
+ Request req = new Request();
+ JsonValue jsonReq = readJson(this.getClass().getClassLoader()
+ .getResource("org/openidentityplatrform/openig/mcp/tools-list-req.json"));
+ req.setEntity(jsonReq.toString());
+
+ MCPServerFeaturesFilter filter = (MCPServerFeaturesFilter) new MCPServerFeaturesFilter
+ .Heaplet().create(Name.of("this"), config, heap);
+
+ Context ctx = new RootContext();
+
+ when(testHandler.handle(ctx, req))
+ .then((Answer>) invocation -> {
+ JsonValue jsonResp = readJson(this.getClass().getClassLoader()
+ .getResource("org/openidentityplatrform/openig/mcp/tools-list-resp.json"));
+ Response resp = new Response(Status.OK);
+ resp.setEntity(jsonResp.toString());
+ return Promises.newResultPromise(resp);
+ });
+
+ Response response = filter.filter(ctx, req, testHandler).get();
+
+ JsonValue result = json(response.getEntity().getJson()).get("result");
+
+ List toolsList = result.get("tools").asList()
+ .stream().map(JsonValue::json).collect(Collectors.toList());
+
+ assertTrue(toolsList.stream().anyMatch(t -> t.get("name").asString().equals("current_time_service")));
+ assertTrue(toolsList.stream().noneMatch(t -> t.get("name").asString().equals("set_current_time_service")));
+ }
+
+ @Test
+ public void testAllowedToolCall() throws Exception {
+ Request req = new Request();
+ JsonValue jsonReq = readJson(this.getClass().getClassLoader()
+ .getResource("org/openidentityplatrform/openig/mcp/allowed-tool-call-req.json"));
+ req.setEntity(jsonReq.toString());
+
+ MCPServerFeaturesFilter filter = (MCPServerFeaturesFilter) new MCPServerFeaturesFilter
+ .Heaplet().create(Name.of("this"), config, heap);
+
+ Context ctx = new RootContext();
+
+ when(testHandler.handle(ctx, req))
+ .then((Answer>) invocation -> {
+ JsonValue jsonResp = readJson(this.getClass().getClassLoader()
+ .getResource("org/openidentityplatrform/openig/mcp/allowed-tool-call-resp.json"));
+ Response resp = new Response(Status.OK);
+ resp.setEntity(jsonResp.toString());
+ return Promises.newResultPromise(resp);
+ });
+
+ Response response = filter.filter(ctx, req, testHandler).get();
+ JsonValue result = json(response.getEntity().getJson()).get("result");
+ assertThat(result.get("isError").asBoolean()).isFalse();
+ assertThat(result.get("content").asList()).isNotEmpty();
+ }
+
+ @Test
+ public void testDeniedToolCall() throws Exception {
+ Request req = new Request();
+ JsonValue jsonReq = readJson(this.getClass().getClassLoader()
+ .getResource("org/openidentityplatrform/openig/mcp/disallowed-tool-call-req.json"));
+ req.setEntity(jsonReq.toString());
+
+ MCPServerFeaturesFilter filter = (MCPServerFeaturesFilter) new MCPServerFeaturesFilter
+ .Heaplet().create(Name.of("this"), config, heap);
+
+ Context ctx = new RootContext();
+
+ when(testHandler.handle(ctx, req))
+ .then((Answer>) invocation -> {
+ JsonValue jsonResp = json(object(field("content", array()), field("isError", false)));
+ Response resp = new Response(Status.OK);
+ resp.setEntity(jsonResp.toString());
+ return Promises.newResultPromise(resp);
+ });
+
+ Response response = filter.filter(ctx, req, testHandler).get();
+ JsonValue responseEntity = json(response.getEntity().getJson());
+ assertThat(responseEntity.get("error").get("code").asInteger()).isNotZero();
+ assertThat(responseEntity.get("error").get("message").asString()).contains("invalid_tool_name");
+ }
+}
\ No newline at end of file
diff --git a/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/allowed-tool-call-req.json b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/allowed-tool-call-req.json
new file mode 100644
index 000000000..0d5568419
--- /dev/null
+++ b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/allowed-tool-call-req.json
@@ -0,0 +1,10 @@
+{
+ "method": "tools/call",
+ "params": {
+ "name": "current_time_service",
+ "arguments": {},
+ "_meta": {
+ "progressToken": 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/allowed-tool-call-resp.json b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/allowed-tool-call-resp.json
new file mode 100644
index 000000000..1ee6d4982
--- /dev/null
+++ b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/allowed-tool-call-resp.json
@@ -0,0 +1,13 @@
+{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "result": {
+ "content": [
+ {
+ "type": "text",
+ "text": "2026-02-05T11:30:30.048135Z"
+ }
+ ],
+ "isError": false
+ }
+}
\ No newline at end of file
diff --git a/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/disallowed-tool-call-req.json b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/disallowed-tool-call-req.json
new file mode 100644
index 000000000..9f51eba0c
--- /dev/null
+++ b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/disallowed-tool-call-req.json
@@ -0,0 +1,12 @@
+{
+ "method": "tools/call",
+ "params": {
+ "name": "set_current_time_service",
+ "arguments": {
+ "timeStr": "2026-02-05T11:30:30.048135Z"
+ },
+ "_meta": {
+ "progressToken": 2
+ }
+ }
+}
\ No newline at end of file
diff --git a/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/tools-list-req.json b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/tools-list-req.json
new file mode 100644
index 000000000..e1441e2ae
--- /dev/null
+++ b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/tools-list-req.json
@@ -0,0 +1,6 @@
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "tools/list",
+ "params": {}
+}
\ No newline at end of file
diff --git a/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/tools-list-resp.json b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/tools-list-resp.json
new file mode 100644
index 000000000..2505ae3f3
--- /dev/null
+++ b/openig-core/src/test/resources/org/openidentityplatrform/openig/mcp/tools-list-resp.json
@@ -0,0 +1,49 @@
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "tools": [
+ {
+ "name": "current_time_service",
+ "title": "current_time_service",
+ "description": "Returns current time in ISO 8601 format",
+ "inputSchema": {
+ "type": "object",
+ "properties": {},
+ "required": []
+ },
+ "annotations": {
+ "title": "",
+ "readOnlyHint": false,
+ "destructiveHint": true,
+ "idempotentHint": false,
+ "openWorldHint": true
+ }
+ },
+ {
+ "name": "set_current_time_service",
+ "title": "set_current_time_service",
+ "description": "Sets the current time in ISO 8601 format",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "timeStr": {
+ "type": "string",
+ "description": "new server time"
+ }
+ },
+ "required": [
+ "timeStr"
+ ]
+ },
+ "annotations": {
+ "title": "",
+ "readOnlyHint": false,
+ "destructiveHint": true,
+ "idempotentHint": false,
+ "openWorldHint": true
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/openig-doc/src/main/asciidoc/reference/filters-conf.adoc b/openig-doc/src/main/asciidoc/reference/filters-conf.adoc
index eaf7340aa..705b4c842 100644
--- a/openig-doc/src/main/asciidoc/reference/filters-conf.adoc
+++ b/openig-doc/src/main/asciidoc/reference/filters-conf.adoc
@@ -809,6 +809,100 @@ See also xref:expressions-conf.adoc#Expressions[Expressions(5)].
==== Javadoc
link:{apidocs-url}/index.html?org/forgerock/openig/filter/LocationHeaderFilter.html[org.forgerock.openig.filter.LocationHeaderFilter, window=\_blank]
+'''
+[#MCPServerFeaturesFilter]
+=== MCPServerFeaturesFilter — restrict access to MCP server features.
+
+[#MCPServerFeaturesFilter-description]
+An MCPServerFeaturesFilter enforces allow/deny policies for MCP (https://modelcontextprotocol.io/specification/2025-11-25[Model Context Protocol, target="_blank"]):
+features exchanged as JSON-RPC payloads with an MCP server. It inspects both
+incoming requests and outgoing responses and removes or rejects features
+according to the configured rules.
+
+==== Usage
+
+[source, json]
+----
+{
+ "type": "MCPFeaturesFilter",
+ "config": {
+ "allow": {
+ "tools": [tools names list],
+ "prompts": [prompts names list],
+ "resources": [resources URIs list],
+ "resources/templates": [resources templates URIs list]
+ },
+ "deny": {
+ "tools": [tools names list],
+ "prompts": [prompts names list],
+ "resources": [resources URIs list],
+ "resources/templates": [resources templates URIs list]
+ }
+ }
+}
+----
+
+==== Properties
+
+`"allow"`: __allowed MCP server features, optional__::
+This object represents allowed MCP server features:
++
+[open]
+====
+
+`"tools"`: __array of strings, optional__::
+A list of allowed MCP server tool names
+`"prompts"`: __array of strings, optional__::
+A list of allowed MCP server prompt names
+`"resources"`: __array of strings, optional__::
+A list of allowed MCP server resource URIs
+`"resources/templates"` :__array of strings, optional__::
+A list of allowed MCP server resources template URIs
+====
++
+Default: Empty object.
+
+`"deny"`: __denied MCP server features, optional__::
+This object represents denied MCP server features:
++
+[open]
+====
+
+`"tools"`: __array of strings, optional__::
+A list of denied MCP server tool names
+`"prompts"`: __array of strings, optional__::
+A list of denied MCP server prompt names
+`"resources"`: __array of strings, optional__::
+A list of denied MCP server resource URIs
+`"resources/templates"` :__array of strings, optional__::
+A list of denied MCP server resource templates URIs
+====
++
+Default: Empty object.
+
+
+==== Example
+
+[source, json]
+----
+{
+ "type": "MCPFeaturesFilter",
+ "config": {
+ "allow": {
+ "tools": ["get_weather"],
+ "prompts": ["code_review"]
+ },
+ "deny": {
+ "resources": ["file:///project/src/main.rs"]
+ }
+ }
+}
+----
+
+==== Javadoc
+link:{apidocs-url}/index.html?org/openidentityplatform/openig/filter/MCPServerFeaturesFilter[org.openidentityplatform.openig.filter.MCPServerFeaturesFilter, window=\_blank]
+
+
'''
[#OAuth2ClientFilter]
=== OAuth2ClientFilter — Authenticate an end user with OAuth 2.0 delegated authorization