- Overview
- Architecture
- Policy Integration
- Tool Filtering Workflow
- API Reference
- Usage Examples
- Configuration
- Troubleshooting
MCP Groups allow you to organize multiple MCP servers into logical collections that act as a unified sub-gateway. Each group provides a single MCP-compliant HTTP endpoint that aggregates tools, resources, and prompts from all member servers.
- Unified Access: Single endpoint for multiple MCP servers
- Role-Based Organization: Create groups for different teams/projects
- Policy Enforcement: Automatic policy-aware tool filtering
- Granular Control: Per-server tool configuration within groups
- Security: Policy restrictions always take precedence
Engineering Team Group
├─ GitHub MCP (issue management, PRs)
├─ Slack MCP (team communication)
└─ Jira MCP (sprint planning)
→ Endpoint: http://localhost:8000/mcp/group/1/mcp
Sales Team Group
├─ Notion MCP (CRM, notes)
├─ Gmail MCP (email)
└─ Calendar MCP (scheduling)
→ Endpoint: http://localhost:8000/mcp/group/2/mcp
DevOps Group
├─ AWS MCP (infrastructure)
├─ Datadog MCP (monitoring)
└─ GitHub MCP (deployments only)
→ Endpoint: http://localhost:8000/mcp/group/3/mcp
┌─────────────────────────────────────────────────────────┐
│ Secure MCP Gateway (Port 8000) │
│ │
│ ┌────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ Policy │ │ Group │ │ PolicyAware │ │
│ │ Engine │ │ Service │ │ ToolService │ │
│ └────────────┘ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌───────▼────────┐ ┌──────▼─────────┐ ┌───▼──────────┐
│ Group 1 │ │ Group 2 │ │ Group 3 │
│ /mcp/group/1/ │ │ /mcp/group/2/ │ │ /mcp/group/3/│
│ mcp │ │ mcp │ │ mcp │
└────────────────┘ └────────────────┘ └──────────────┘
│ │ │
┌───┴────┐ ┌────┴────┐ ┌───┴────┐
│ GitHub │ │ Notion │ │ AWS │
│ Slack │ │ Gmail │ │Datadog │
│ Jira │ │Calendar │ │ │
└────────┘ └─────────┘ └────────┘
Table: mcp_server_groups
CREATE TABLE mcp_server_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
server_names TEXT, -- JSON array of server names
tool_config TEXT, -- JSON object: { serverName: [tools] }
gateway_url VARCHAR(1024), -- Auto-generated endpoint URL
gateway_port INTEGER, -- Gateway port (default: 8000)
enabled BOOLEAN DEFAULT true,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);Key Fields:
server_names: JSON array["github", "slack", "jira"]tool_config: JSON object defining which tools are exposed per server{ "github": ["create_issue", "list_repos"], "slack": ["*"], "jira": null }gateway_url: Auto-generated ashttp://{host}:{port}/mcp/group/{id}/mcp
File: entity/McpServerGroupEntity.java
@Entity
@Table(name = "mcp_server_groups")
public class McpServerGroupEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
@Column(name = "server_names", columnDefinition = "TEXT")
private String serverNamesJson; // Stored as JSON
@Column(name = "tool_config", columnDefinition = "TEXT")
private String toolConfigJson; // Stored as JSON
private String gatewayUrl;
private Integer gatewayPort;
private Boolean enabled;
// Helper methods
public List<String> getServerNamesList() { ... }
public void setServerNamesList(List<String> names) { ... }
public Map<String, List<String>> getToolConfig() { ... }
public void setToolConfig(Map<String, List<String>> config) { ... }
}File: service/McpGroupService.java
Handles CRUD operations and validation:
@Service
public class McpGroupService {
// Group Management
public List<Map<String, Object>> getAllGroups();
public Map<String, Object> getGroup(String groupId);
public Map<String, Object> createGroup(Map<String, Object> groupData);
public Map<String, Object> updateGroup(String groupId, Map<String, Object> updates);
public void deleteGroup(String groupId);
// Server Management
public Map<String, Object> addServerToGroup(String groupId, String serverName);
public Map<String, Object> removeServerFromGroup(String groupId, String serverName);
// Tool Configuration
public Map<String, Object> configureServerTools(String groupId,
String serverName,
List<String> tools);
}File: service/PolicyAwareToolService.java
Handles policy-aware tool filtering:
@Service
public class PolicyAwareToolService {
/**
* Get tools allowed by policy for a server
*/
public Mono<List<String>> getPolicyAllowedTools(String serverName, String username);
/**
* Apply both policy AND group filtering
* Formula: Available = Server Tools ∩ Policy-Allowed ∩ Group-Configured
*/
public Mono<List<Map<String, Object>>> getAvailableTools(
String serverName,
String username,
List<String> groupConfiguredTools
);
}File: controller/McpController.java
Exposes REST APIs for group management and MCP protocol endpoints.
There are two independent mechanisms for restricting tool access:
-
Policy-Level Restrictions (via Policy Engine)
- Authoritative security layer
- Defines which tools users can access
- Example: User can only access read-only GitHub tools
-
Group-Level Configuration (via MCP Groups)
- Convenience/UX layer
- Reduces clutter by exposing only relevant tools
- Example: Engineering group only shows code-related tools
Principle: Policy restrictions take absolute precedence over group configurations.
┌─────────────────────────────────────────────────────┐
│ Tool Availability Decision Flow │
└─────────────────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ MCP Server Has │
│ 100 Tools │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Policy Filtering │
│ (AUTHORITATIVE) │
│ → 10 tools allowed │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Group Filtering │
│ (OPTIONAL) │
│ → 5 tools configured │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ FINAL: 5 tools │
│ (intersection) │
└───────────────────────┘
Available Tools = Server Tools ∩ Policy-Allowed Tools ∩ Group-Configured Tools
Where:
Server Tools: All tools the MCP server providesPolicy-Allowed Tools: Tools allowed by active policies for the userGroup-Configured Tools: Tools configured in the group'stool_config
Server has: [tool1, tool2, tool3, tool4, tool5]
Policy allows: [tool1, tool2, tool3]
Group config: [tool1, tool2, tool3, tool4]
Result: [tool1, tool2, tool3] ← Policy wins
Server has: [tool1, tool2, tool3, tool4, tool5]
Policy allows: [tool1, tool2, tool3, tool4]
Group config: [tool1, tool2]
Result: [tool1, tool2] ← Group config wins
Server has: [tool1, tool2, tool3]
Policy allows: [tool1, tool2]
Group config: [tool3, tool4]
Result: [] ← Empty! User sees warning
Server has: [tool1, tool2, tool3]
Policy allows: [tool1, tool2]
Group config: null or []
Result: [tool1, tool2] ← Policy applies, no group filtering
Frontend: MCPServers.tsx
const openToolConfigDialog = async (groupId, serverName, group) => {
// Fetch policy-allowed tools (not all tools)
const response = await javaGatewayMcpApi.getPolicyAllowedTools(serverName);
const toolNames = response.tools.map(tool => tool.name);
setAvailableTools(toolNames);
setPolicyFilteredTools(response.policy_filtered);
setTotalServerTools(response.total_server_tools);
// Check for invalid configured tools
const currentTools = group.tool_config?.[serverName] || [];
const invalidTools = currentTools.filter(tool => !toolNames.includes(tool));
if (invalidTools.length > 0) {
setInvalidToolsWarning(invalidTools); // Show warning
}
// Only show valid tools
const validSelectedTools = currentTools.filter(tool => toolNames.includes(tool));
setSelectedTools(validSelectedTools);
};UI Indicators:
- ✅ Blue Info Banner: "Policy Filtering Active: Showing 8 of 22 tools"
⚠️ Red Warning Banner: "The following tools are not allowed by policy: [delete_repo, force_push]"- 📝 Description: "Only tools allowed by your policies are shown"
Endpoint: GET /mcp/servers/{serverName}/policy-allowed-tools
Controller: McpController.java
@GetMapping("/servers/{serverName}/policy-allowed-tools")
public Mono<ResponseEntity<Map<String, Object>>> getPolicyAllowedTools(
@PathVariable String serverName) {
String username = authService.getUsername();
return policyAwareToolService.getPolicyAllowedTools(serverName, username)
.flatMap(allowedToolNames -> {
return mcpProxyService.listTools(serverName, username)
.map(result -> {
List<Map<String, Object>> allTools = result.get("tools");
// Filter to only policy-allowed tools
List<Map<String, Object>> allowedTools = allTools.stream()
.filter(tool -> allowedToolNames.contains(tool.get("name")))
.collect(Collectors.toList());
return ResponseEntity.ok(Map.of(
"server_name", serverName,
"username", username,
"tools", allowedTools,
"count", allowedTools.size(),
"total_server_tools", allTools.size(),
"policy_filtered", true
));
});
});
}Policy Engine: GET /api/v1/unified/resources/mcp_server/{serverName}/policies
Response Example:
{
"count": 1,
"policies": [
{
"policy_id": "abc123",
"status": "active",
"policy_rules": [
{
"actions": [{"type": "allow"}]
}
],
"resources": [
{"resource_type": "mcp_server", "resource_id": "github"},
{"resource_type": "tool", "resource_id": "github:create_issue"},
{"resource_type": "tool", "resource_id": "github:list_repos"},
{"resource_type": "tool", "resource_id": "github:get_pr"}
]
}
]
}Service: PolicyAwareToolService.java
// Extract tool resources from policy
for (Map<String, Object> resource : resources) {
String resourceType = resource.get("resource_type");
String resourceId = resource.get("resource_id");
if ("tool".equals(resourceType) && resourceId != null) {
// Resource ID format: "serverName:toolName"
String[] parts = resourceId.split(":", 2);
if (parts.length == 2 && serverName.equals(parts[0])) {
String toolName = parts[1];
policyAllowedTools.add(toolName);
}
}
}Result: ["create_issue", "list_repos", "get_pr"]
Request: POST /mcp/group/3/mcp
{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}Controller: McpController.handleGroupListTools()
// Get group configuration
Map<String, Object> group = mcpGroupService.getGroup(groupId);
List<String> serverNames = group.get("serverNames");
Map<String, List<String>> toolConfig = group.get("tool_config");
// For each server in the group
return Flux.fromIterable(serverNames)
.flatMap(serverName -> {
List<String> groupConfiguredTools = toolConfig.get(serverName);
// Apply policy + group filtering
return policyAwareToolService.getAvailableTools(
serverName,
username,
groupConfiguredTools
);
})
.collectList()
.map(toolLists -> {
// Aggregate all tools from all servers
List<Map<String, Object>> allTools = toolLists.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
return Map.of("tools", allTools);
});Service: PolicyAwareToolService.getAvailableTools()
List<Map<String, Object>> filteredTools = allTools.stream()
.filter(tool -> {
String toolName = tool.get("name");
// 1. Must be allowed by policy
boolean allowedByPolicy = policyAllowedTools.contains(toolName);
// 2. Must be in group config (if configured)
boolean allowedByGroup = groupConfiguredTools == null
|| groupConfiguredTools.isEmpty()
|| groupConfiguredTools.contains("*")
|| groupConfiguredTools.contains(toolName);
// Intersection: BOTH must be true
return allowedByPolicy && allowedByGroup;
})
.collect(Collectors.toList());Result: Only tools that satisfy both policy and group requirements
GET /mcp/groupsResponse:
{
"groups": [
{
"id": "1",
"name": "Engineering Team",
"description": "Tools for software development",
"serverNames": ["github", "slack", "jira"],
"server_count": 3,
"gateway_url": "http://localhost:8000/mcp/group/1/mcp",
"gateway_port": 8000,
"enabled": true,
"created_at": "2026-02-14T10:00:00Z",
"updated_at": "2026-02-14T10:00:00Z"
}
],
"count": 1
}GET /mcp/groups/{groupId}Response:
{
"id": "1",
"name": "Engineering Team",
"description": "Tools for software development",
"serverNames": ["github", "slack", "jira"],
"tool_config": {
"github": ["create_issue", "list_repos"],
"slack": ["*"],
"jira": null
},
"server_count": 3,
"gateway_url": "http://localhost:8000/mcp/group/1/mcp",
"gateway_port": 8000,
"enabled": true,
"created_at": "2026-02-14T10:00:00Z",
"updated_at": "2026-02-14T10:00:00Z"
}POST /mcp/groups
Content-Type: application/json
{
"name": "Engineering Team",
"description": "Tools for software development",
"serverNames": ["github", "slack"]
}Response:
{
"success": true,
"message": "Group created successfully",
"group": { ... }
}Validation Rules:
- ✅ Name must be unique
- ✅ Name can only contain alphanumeric, spaces, hyphens, underscores
- ✅ All servers must be HTTP type (STDIO not allowed)
PUT /mcp/groups/{groupId}
Content-Type: application/json
{
"name": "Updated Name",
"description": "Updated description",
"serverNames": ["github", "slack", "jira"]
}DELETE /mcp/groups/{groupId}Response:
{
"success": true,
"message": "Group deleted successfully"
}POST /mcp/groups/{groupId}/servers/{serverName}Response:
{
"success": true,
"message": "Server added to group",
"group": { ... }
}DELETE /mcp/groups/{groupId}/servers/{serverName}POST /mcp/groups/{groupId}/servers
Content-Type: application/json
{
"serverNames": ["github", "slack", "jira"]
}PUT /mcp/groups/{groupId}/servers/{serverName}/tools
Content-Type: application/json
{
"tools": ["create_issue", "list_repos", "get_pr"]
}Special Values:
["*"]: Allow all tools (subject to policy)[]ornull: Allow all tools (subject to policy)
Response:
{
"success": true,
"message": "Tool configuration updated",
"group": { ... }
}GET /mcp/servers/{serverName}/policy-allowed-toolsResponse:
{
"server_name": "github",
"username": "testuser",
"tools": [
{"name": "create_issue", "description": "...", "inputSchema": {...}},
{"name": "list_repos", "description": "...", "inputSchema": {...}}
],
"count": 2,
"total_server_tools": 10,
"policy_filtered": true
}GET /mcp/group/{groupId}/mcpResponse: HTML page with MCP server info and connection instructions
POST /mcp/group/{groupId}/mcp
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "example-client",
"version": "1.0.0"
}
}
}Supported Methods:
initialize: Initialize connectiontools/list: List all tools from group serverstools/call: Invoke a toolresources/list: List resourcesresources/read: Read a resourceprompts/list: List promptsprompts/get: Get a prompt
# 1. Create the group
curl -X POST http://localhost:8000/mcp/groups \
-H "Content-Type: application/json" \
-d '{
"name": "Engineering Team",
"description": "Development tools",
"serverNames": ["github", "slack"]
}'
# Response includes gateway_url: http://localhost:8000/mcp/group/1/mcp# Only expose specific GitHub tools
curl -X PUT http://localhost:8000/mcp/groups/1/servers/github/tools \
-H "Content-Type: application/json" \
-d '{
"tools": ["create_issue", "list_repos", "get_pr"]
}'VS Code MCP Settings:
{
"mcpServers": {
"engineering-gateway": {
"url": "http://localhost:8000/mcp/group/1/mcp",
"transport": {
"type": "http"
}
}
}
}curl -X POST http://localhost:8000/mcp/group/1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}'
# Response: Aggregated tools from github + slack (policy-filtered)
{
"jsonrpc": "2.0",
"result": {
"tools": [
{"name": "create_issue", ...},
{"name": "list_repos", ...},
{"name": "send_message", ...}
]
},
"id": 1
}Scenario: GitHub server has 50 tools, policy allows 10, group configures 5
# 1. Check what policy allows
curl http://localhost:8000/mcp/servers/github/policy-allowed-tools
# Returns: 10 tools
# 2. Configure group with 5 tools (all within policy)
curl -X PUT http://localhost:8000/mcp/groups/1/servers/github/tools \
-d '{"tools": ["create_issue", "list_repos", "get_pr", "close_issue", "add_comment"]}'
# 3. List tools via group
curl -X POST http://localhost:8000/mcp/group/1/mcp \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Returns: 5 tools (intersection of policy + group config)# Gateway Configuration
POLICY_ENGINE_URL: http://host.docker.internal:9000
# Database
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mcp_gateway
SPRING_DATASOURCE_USERNAME: mcp_user
SPRING_DATASOURCE_PASSWORD: mcp_password
# Server
SERVER_PORT: 8000
GATEWAY_HOST: localhostservices:
mcp-gateway-java:
environment:
POLICY_ENGINE_URL: http://host.docker.internal:9000
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mcp_gateway
SERVER_PORT: 8000
ports:
- "8000:8000"gateway:
host: ${GATEWAY_HOST:localhost}
policy-engine-url: ${POLICY_ENGINE_URL:http://localhost:9000}Symptoms: Group exposes more tools than policy allows
Diagnosis:
# Check policy engine connection
docker logs mcp-gateway-java | grep "Policy Engine client initialized"
# Expected: http://host.docker.internal:9000 (NOT localhost:9000)Solution:
# docker-compose.yml
environment:
POLICY_ENGINE_URL: http://host.docker.internal:9000Verify:
# Should return policy count > 0
curl "http://localhost:9000/api/v1/unified/resources/mcp_server/github/policies?active=true"Symptoms: Cannot add STDIO server to group
Reason: Groups only support HTTP servers
Solution:
- Convert STDIO server to HTTP using stdio-proxy service
- Go to MCP Servers page → Click "Convert to HTTP" on the server
- Wait for conversion to complete
- Add the converted server to the group
Symptoms: Group returns 0 tools
Diagnosis:
# Check if there's a policy-group mismatch
docker logs mcp-gateway-java | grep "POLICY-GROUP MISMATCH"Common Causes:
- No overlap: Policy allows
[tool1, tool2]but group configures[tool3, tool4] - Wrong tool names: Group uses incorrect tool names not matching MCP server
- Policy blocks all: Policy has no active rules for the server
Solution:
- Check actual tool names:
GET /mcp/servers/{name}/policy-allowed-tools - Update group config with correct tool names
- Verify policy allows at least some tools
Symptoms: UI tool configuration dialog shows more tools than policy allows
Diagnosis:
- Check if frontend is calling the correct endpoint
- Verify backend is using
PolicyAwareToolService
Solution: Ensure frontend uses:
// CORRECT
const response = await javaGatewayMcpApi.getPolicyAllowedTools(serverName);
// WRONG
const response = await javaGatewayMcpApi.listTools(serverName);curl "http://localhost:8000/mcp/servers/github/tool-availability-debug?group_id=1"Response:
{
"server_name": "github",
"total_tools": 50,
"policy_allowed_tools": ["create_issue", "list_repos", "get_pr"],
"group_configured_tools": ["create_issue", "list_repos"],
"final_available_tools": ["create_issue", "list_repos"],
"blocked_by_policy": ["delete_repo", "force_push", ...],
"blocked_by_group": ["get_pr"],
"username": "testuser"
}✅ DO: Organize by team/role
Engineering Group → GitHub, Slack, Jira
Sales Group → Notion, Gmail, Calendar
DevOps Group → AWS, Datadog, GitHub
❌ DON'T: Mix unrelated servers
Random Group → GitHub, Gmail, AWS, Slack, Notion
✅ DO: Be specific when needed
{
"github": ["create_issue", "list_repos", "get_pr"],
"slack": ["send_message", "list_channels"]
}✅ DO: Use wildcard for full access (within policy)
{
"github": ["*"]
}❌ DON'T: Configure tools not allowed by policy
// Policy allows: ["create_issue", "list_repos"]
{
"github": ["delete_repo"] // ❌ Will be filtered out
}✅ DO: Start restrictive, expand as needed
Initial: Allow read-only tools
After review: Add write operations
✅ DO: Document why tools are restricted
Policy: "github-read-only"
Reason: "Junior developers should only read code, not modify"
✅ DO: Test policy filtering
# 1. Create policy with restrictions
# 2. Create group
# 3. Verify tools/list returns only allowed tools
# 4. Try to invoke restricted tool (should fail)✅ DO: Monitor logs for mismatches
docker logs mcp-gateway-java | grep "POLICY-GROUP MISMATCH"- MCP Groups organize multiple MCP servers into unified sub-gateways
- Policy restrictions always take precedence over group configurations
- Tool filtering uses intersection:
Server ∩ Policy ∩ Group - HTTP-only: Groups can only contain HTTP-type MCP servers
- Auto-generated URLs: Each group gets
http://{host}:{port}/mcp/group/{id}/mcp - Policy integration: Automatic filtering ensures security policies are enforced
Key Files:
- Entity:
entity/McpServerGroupEntity.java - Service:
service/McpGroupService.java,service/PolicyAwareToolService.java - Controller:
controller/McpController.java - Migration:
db/migration/V4__mcp_server_groups.sql
For More Information:
- README.md - General gateway documentation
- QUICKSTART.md - Getting started guide
- VSCODE_INTEGRATION.md - VS Code setup