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: + *

+ * + *
+ * {
+ *     "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