agent-mcp-gateway 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of agent-mcp-gateway might be problematic. Click here for more details.
- agent_mcp_gateway-0.1.5.dist-info/METADATA +1418 -0
- agent_mcp_gateway-0.1.5.dist-info/RECORD +18 -0
- agent_mcp_gateway-0.1.5.dist-info/WHEEL +4 -0
- agent_mcp_gateway-0.1.5.dist-info/entry_points.txt +2 -0
- agent_mcp_gateway-0.1.5.dist-info/licenses/LICENSE +21 -0
- src/CONFIG_README.md +351 -0
- src/__init__.py +1 -0
- src/audit.py +94 -0
- src/config/.mcp-gateway-rules.json.example +45 -0
- src/config/.mcp.json.example +30 -0
- src/config.py +837 -0
- src/config_watcher.py +296 -0
- src/gateway.py +547 -0
- src/main.py +556 -0
- src/metrics.py +299 -0
- src/middleware.py +166 -0
- src/policy.py +494 -0
- src/proxy.py +649 -0
src/metrics.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Metrics collection for Agent MCP Gateway."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class OperationMetrics:
|
|
10
|
+
"""Metrics for a specific operation.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
count: Total number of operations recorded
|
|
14
|
+
total_latency_ms: Cumulative latency in milliseconds
|
|
15
|
+
latencies: List of individual latency measurements
|
|
16
|
+
errors: Number of operations that resulted in errors
|
|
17
|
+
"""
|
|
18
|
+
count: int = 0
|
|
19
|
+
total_latency_ms: float = 0.0
|
|
20
|
+
latencies: List[float] = field(default_factory=list)
|
|
21
|
+
errors: int = 0
|
|
22
|
+
|
|
23
|
+
def record(self, latency_ms: float, is_error: bool = False):
|
|
24
|
+
"""Record a single operation.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
latency_ms: Operation latency in milliseconds
|
|
28
|
+
is_error: Whether the operation resulted in an error
|
|
29
|
+
"""
|
|
30
|
+
self.count += 1
|
|
31
|
+
self.total_latency_ms += latency_ms
|
|
32
|
+
self.latencies.append(latency_ms)
|
|
33
|
+
if is_error:
|
|
34
|
+
self.errors += 1
|
|
35
|
+
|
|
36
|
+
def get_summary(self) -> dict:
|
|
37
|
+
"""Generate summary statistics for this operation.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dictionary containing count, avg, percentiles, and error_rate
|
|
41
|
+
"""
|
|
42
|
+
if self.count == 0:
|
|
43
|
+
return {
|
|
44
|
+
"count": 0,
|
|
45
|
+
"avg_latency_ms": 0.0,
|
|
46
|
+
"p50_latency_ms": 0.0,
|
|
47
|
+
"p95_latency_ms": 0.0,
|
|
48
|
+
"p99_latency_ms": 0.0,
|
|
49
|
+
"error_rate": 0.0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
avg_latency = self.total_latency_ms / self.count
|
|
53
|
+
error_rate = self.errors / self.count
|
|
54
|
+
|
|
55
|
+
# Calculate percentiles
|
|
56
|
+
sorted_latencies = sorted(self.latencies)
|
|
57
|
+
p50 = self._percentile(sorted_latencies, 50)
|
|
58
|
+
p95 = self._percentile(sorted_latencies, 95)
|
|
59
|
+
p99 = self._percentile(sorted_latencies, 99)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"count": self.count,
|
|
63
|
+
"avg_latency_ms": round(avg_latency, 2),
|
|
64
|
+
"p50_latency_ms": round(p50, 2),
|
|
65
|
+
"p95_latency_ms": round(p95, 2),
|
|
66
|
+
"p99_latency_ms": round(p99, 2),
|
|
67
|
+
"error_rate": round(error_rate, 4)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _percentile(sorted_values: List[float], percentile: int) -> float:
|
|
72
|
+
"""Calculate percentile from sorted values.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
sorted_values: List of values sorted in ascending order
|
|
76
|
+
percentile: Percentile to calculate (0-100)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Value at the specified percentile
|
|
80
|
+
"""
|
|
81
|
+
if not sorted_values:
|
|
82
|
+
return 0.0
|
|
83
|
+
|
|
84
|
+
if len(sorted_values) == 1:
|
|
85
|
+
return sorted_values[0]
|
|
86
|
+
|
|
87
|
+
# Use linear interpolation method
|
|
88
|
+
k = (len(sorted_values) - 1) * (percentile / 100.0)
|
|
89
|
+
f = int(k)
|
|
90
|
+
c = f + 1
|
|
91
|
+
|
|
92
|
+
if c >= len(sorted_values):
|
|
93
|
+
return sorted_values[-1]
|
|
94
|
+
|
|
95
|
+
# Interpolate between floor and ceiling
|
|
96
|
+
d0 = sorted_values[f] * (c - k)
|
|
97
|
+
d1 = sorted_values[c] * (k - f)
|
|
98
|
+
|
|
99
|
+
return d0 + d1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MetricsCollector:
|
|
103
|
+
"""Collects and aggregates metrics for gateway operations.
|
|
104
|
+
|
|
105
|
+
Thread-safe metrics collection with per-agent and per-operation tracking.
|
|
106
|
+
Metrics are stored in memory and can be aggregated for monitoring.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
"""Initialize metrics collector with empty storage."""
|
|
111
|
+
# Overall metrics: operation -> OperationMetrics
|
|
112
|
+
self._metrics: Dict[str, OperationMetrics] = {}
|
|
113
|
+
|
|
114
|
+
# Per-agent metrics: agent_id -> operation -> OperationMetrics
|
|
115
|
+
self._agent_metrics: Dict[str, Dict[str, OperationMetrics]] = {}
|
|
116
|
+
|
|
117
|
+
# Lock for thread-safe operations
|
|
118
|
+
self._lock = asyncio.Lock()
|
|
119
|
+
|
|
120
|
+
async def record(
|
|
121
|
+
self,
|
|
122
|
+
agent_id: str,
|
|
123
|
+
operation: str,
|
|
124
|
+
latency_ms: float,
|
|
125
|
+
is_error: bool = False
|
|
126
|
+
):
|
|
127
|
+
"""Record a single operation metric.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
agent_id: Agent identifier
|
|
131
|
+
operation: Operation name (list_servers, execute_tool, etc.)
|
|
132
|
+
latency_ms: Operation latency in milliseconds
|
|
133
|
+
is_error: Whether the operation resulted in an error
|
|
134
|
+
"""
|
|
135
|
+
async with self._lock:
|
|
136
|
+
# Record overall metrics
|
|
137
|
+
if operation not in self._metrics:
|
|
138
|
+
self._metrics[operation] = OperationMetrics()
|
|
139
|
+
self._metrics[operation].record(latency_ms, is_error)
|
|
140
|
+
|
|
141
|
+
# Record per-agent metrics
|
|
142
|
+
if agent_id not in self._agent_metrics:
|
|
143
|
+
self._agent_metrics[agent_id] = {}
|
|
144
|
+
if operation not in self._agent_metrics[agent_id]:
|
|
145
|
+
self._agent_metrics[agent_id][operation] = OperationMetrics()
|
|
146
|
+
self._agent_metrics[agent_id][operation].record(latency_ms, is_error)
|
|
147
|
+
|
|
148
|
+
def record_sync(
|
|
149
|
+
self,
|
|
150
|
+
agent_id: str,
|
|
151
|
+
operation: str,
|
|
152
|
+
latency_ms: float,
|
|
153
|
+
is_error: bool = False
|
|
154
|
+
):
|
|
155
|
+
"""Record a single operation metric (synchronous version).
|
|
156
|
+
|
|
157
|
+
Note: This is not thread-safe. Use async record() for concurrent access.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
agent_id: Agent identifier
|
|
161
|
+
operation: Operation name (list_servers, execute_tool, etc.)
|
|
162
|
+
latency_ms: Operation latency in milliseconds
|
|
163
|
+
is_error: Whether the operation resulted in an error
|
|
164
|
+
"""
|
|
165
|
+
# Record overall metrics
|
|
166
|
+
if operation not in self._metrics:
|
|
167
|
+
self._metrics[operation] = OperationMetrics()
|
|
168
|
+
self._metrics[operation].record(latency_ms, is_error)
|
|
169
|
+
|
|
170
|
+
# Record per-agent metrics
|
|
171
|
+
if agent_id not in self._agent_metrics:
|
|
172
|
+
self._agent_metrics[agent_id] = {}
|
|
173
|
+
if operation not in self._agent_metrics[agent_id]:
|
|
174
|
+
self._agent_metrics[agent_id][operation] = OperationMetrics()
|
|
175
|
+
self._agent_metrics[agent_id][operation].record(latency_ms, is_error)
|
|
176
|
+
|
|
177
|
+
async def get_summary(self) -> dict:
|
|
178
|
+
"""Get overall summary of all operations.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dictionary mapping operation names to their summary statistics
|
|
182
|
+
"""
|
|
183
|
+
async with self._lock:
|
|
184
|
+
return self._get_summary_internal()
|
|
185
|
+
|
|
186
|
+
def get_summary_sync(self) -> dict:
|
|
187
|
+
"""Get overall summary of all operations (synchronous version).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Dictionary mapping operation names to their summary statistics
|
|
191
|
+
"""
|
|
192
|
+
return self._get_summary_internal()
|
|
193
|
+
|
|
194
|
+
def _get_summary_internal(self) -> dict:
|
|
195
|
+
"""Internal method to get summary without locking."""
|
|
196
|
+
return {
|
|
197
|
+
operation: metrics.get_summary()
|
|
198
|
+
for operation, metrics in self._metrics.items()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async def get_agent_summary(self, agent_id: str) -> dict:
|
|
202
|
+
"""Get summary for a specific agent.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
agent_id: Agent identifier
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Dictionary mapping operation names to summary statistics for this agent,
|
|
209
|
+
or empty dict if agent has no recorded metrics
|
|
210
|
+
"""
|
|
211
|
+
async with self._lock:
|
|
212
|
+
return self._get_agent_summary_internal(agent_id)
|
|
213
|
+
|
|
214
|
+
def get_agent_summary_sync(self, agent_id: str) -> dict:
|
|
215
|
+
"""Get summary for a specific agent (synchronous version).
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
agent_id: Agent identifier
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Dictionary mapping operation names to summary statistics for this agent,
|
|
222
|
+
or empty dict if agent has no recorded metrics
|
|
223
|
+
"""
|
|
224
|
+
return self._get_agent_summary_internal(agent_id)
|
|
225
|
+
|
|
226
|
+
def _get_agent_summary_internal(self, agent_id: str) -> dict:
|
|
227
|
+
"""Internal method to get agent summary without locking."""
|
|
228
|
+
if agent_id not in self._agent_metrics:
|
|
229
|
+
return {}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
operation: metrics.get_summary()
|
|
233
|
+
for operation, metrics in self._agent_metrics[agent_id].items()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async def get_operation_summary(self, operation: str) -> dict:
|
|
237
|
+
"""Get summary for a specific operation.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
operation: Operation name
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Summary statistics for this operation, or empty metrics if not found
|
|
244
|
+
"""
|
|
245
|
+
async with self._lock:
|
|
246
|
+
return self._get_operation_summary_internal(operation)
|
|
247
|
+
|
|
248
|
+
def get_operation_summary_sync(self, operation: str) -> dict:
|
|
249
|
+
"""Get summary for a specific operation (synchronous version).
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
operation: Operation name
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Summary statistics for this operation, or empty metrics if not found
|
|
256
|
+
"""
|
|
257
|
+
return self._get_operation_summary_internal(operation)
|
|
258
|
+
|
|
259
|
+
def _get_operation_summary_internal(self, operation: str) -> dict:
|
|
260
|
+
"""Internal method to get operation summary without locking."""
|
|
261
|
+
if operation not in self._metrics:
|
|
262
|
+
return {
|
|
263
|
+
"count": 0,
|
|
264
|
+
"avg_latency_ms": 0.0,
|
|
265
|
+
"p50_latency_ms": 0.0,
|
|
266
|
+
"p95_latency_ms": 0.0,
|
|
267
|
+
"p99_latency_ms": 0.0,
|
|
268
|
+
"error_rate": 0.0
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return self._metrics[operation].get_summary()
|
|
272
|
+
|
|
273
|
+
async def get_all_agents(self) -> List[str]:
|
|
274
|
+
"""Get list of all agents with recorded metrics.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
List of agent identifiers
|
|
278
|
+
"""
|
|
279
|
+
async with self._lock:
|
|
280
|
+
return list(self._agent_metrics.keys())
|
|
281
|
+
|
|
282
|
+
def get_all_agents_sync(self) -> List[str]:
|
|
283
|
+
"""Get list of all agents with recorded metrics (synchronous version).
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
List of agent identifiers
|
|
287
|
+
"""
|
|
288
|
+
return list(self._agent_metrics.keys())
|
|
289
|
+
|
|
290
|
+
async def reset(self):
|
|
291
|
+
"""Reset all metrics (useful for testing)."""
|
|
292
|
+
async with self._lock:
|
|
293
|
+
self._metrics.clear()
|
|
294
|
+
self._agent_metrics.clear()
|
|
295
|
+
|
|
296
|
+
def reset_sync(self):
|
|
297
|
+
"""Reset all metrics (synchronous version, useful for testing)."""
|
|
298
|
+
self._metrics.clear()
|
|
299
|
+
self._agent_metrics.clear()
|
src/middleware.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Access control middleware for Agent MCP Gateway.
|
|
2
|
+
|
|
3
|
+
This module implements the AgentAccessControl middleware that enforces
|
|
4
|
+
per-agent access rules for gateway tools. It extracts agent identity from
|
|
5
|
+
tool call arguments, validates permissions, and manages agent context state.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
9
|
+
from fastmcp.exceptions import ToolError
|
|
10
|
+
from .policy import PolicyEngine
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentAccessControl(Middleware):
|
|
14
|
+
"""Enforces per-agent access rules for gateway operations.
|
|
15
|
+
|
|
16
|
+
This middleware intercepts tool calls to:
|
|
17
|
+
1. Extract agent_id from arguments
|
|
18
|
+
2. Validate agent identity (handle missing agent_id based on default policy)
|
|
19
|
+
3. Store agent in context state for downstream use
|
|
20
|
+
4. Remove agent_id from arguments before forwarding to tools
|
|
21
|
+
5. Allow gateway tools to perform their own authorization
|
|
22
|
+
|
|
23
|
+
Gateway tools (list_servers, get_server_tools, execute_tool) handle their
|
|
24
|
+
own permission checks, so the middleware just extracts and cleans agent_id
|
|
25
|
+
without blocking them.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, policy_engine: PolicyEngine):
|
|
29
|
+
"""Initialize middleware with policy engine.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
policy_engine: PolicyEngine instance for evaluating access rules
|
|
33
|
+
"""
|
|
34
|
+
self.policy_engine = policy_engine
|
|
35
|
+
|
|
36
|
+
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
|
37
|
+
"""Intercept tool calls to extract and validate agent identity.
|
|
38
|
+
|
|
39
|
+
This hook:
|
|
40
|
+
- Extracts agent_id from tool arguments
|
|
41
|
+
- Validates agent identity based on default policy
|
|
42
|
+
- Applies fallback chain if agent_id is missing (when deny_on_missing_agent: false)
|
|
43
|
+
- Stores agent in context state for downstream tools
|
|
44
|
+
- Keeps agent_id in arguments (gateway tools need it)
|
|
45
|
+
- Allows gateway tools to pass through (they do own auth)
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
context: Middleware context containing the tool call message
|
|
49
|
+
call_next: Callable to invoke next middleware/handler in chain
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Result from downstream handler
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ToolError: If agent_id is missing and default policy denies access,
|
|
56
|
+
or if fallback chain fails to find a valid agent
|
|
57
|
+
"""
|
|
58
|
+
# Extract the tool call message
|
|
59
|
+
tool_call = context.message
|
|
60
|
+
arguments = tool_call.arguments or {}
|
|
61
|
+
|
|
62
|
+
# Extract agent_id from arguments
|
|
63
|
+
agent_id = arguments.get("agent_id")
|
|
64
|
+
|
|
65
|
+
# Handle missing agent_id based on default policy
|
|
66
|
+
if not agent_id:
|
|
67
|
+
# Check if default policy allows missing agents
|
|
68
|
+
deny_on_missing = self.policy_engine.defaults.get("deny_on_missing_agent", True)
|
|
69
|
+
if deny_on_missing:
|
|
70
|
+
raise ToolError(
|
|
71
|
+
"Missing required parameter 'agent_id'. "
|
|
72
|
+
"All tool calls must include agent identity."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Apply fallback chain (deny_on_missing_agent: false)
|
|
76
|
+
# Priority: 1. GATEWAY_DEFAULT_AGENT env var, 2. "default" agent in rules
|
|
77
|
+
agent_id = self._resolve_fallback_agent(context)
|
|
78
|
+
|
|
79
|
+
# Inject the resolved agent_id back into arguments for gateway tools
|
|
80
|
+
if agent_id:
|
|
81
|
+
arguments["agent_id"] = agent_id
|
|
82
|
+
tool_call.arguments = arguments
|
|
83
|
+
|
|
84
|
+
# Store agent in context state for downstream tools
|
|
85
|
+
# This allows gateway tools to access the current agent
|
|
86
|
+
if context.fastmcp_context:
|
|
87
|
+
context.fastmcp_context.set_state("current_agent", agent_id)
|
|
88
|
+
|
|
89
|
+
# NOTE: We do NOT remove agent_id from arguments because the gateway
|
|
90
|
+
# tools (list_servers, get_server_tools, execute_tool) need it as
|
|
91
|
+
# a parameter to perform their authorization checks.
|
|
92
|
+
# If we ever add direct proxying to downstream servers in the future,
|
|
93
|
+
# we would need to remove it at that point.
|
|
94
|
+
|
|
95
|
+
# Gateway tools (list_servers, get_server_tools, execute_tool) are
|
|
96
|
+
# allowed through - they perform their own permission checks using
|
|
97
|
+
# the agent_id parameter
|
|
98
|
+
return await call_next(context)
|
|
99
|
+
|
|
100
|
+
def _resolve_fallback_agent(self, context: MiddlewareContext) -> str:
|
|
101
|
+
"""Resolve fallback agent when agent_id is missing and deny_on_missing_agent: false.
|
|
102
|
+
|
|
103
|
+
Fallback priority order:
|
|
104
|
+
1. GATEWAY_DEFAULT_AGENT environment variable (read from gateway module)
|
|
105
|
+
2. Agent named "default" in gateway rules configuration
|
|
106
|
+
3. Raise helpful error if neither is configured
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
context: Middleware context to access gateway state
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Resolved agent_id from fallback chain
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ToolError: If no fallback agent is configured or if fallback agent doesn't exist in rules
|
|
116
|
+
"""
|
|
117
|
+
# Import here to avoid circular dependency
|
|
118
|
+
from .gateway import get_default_agent_id
|
|
119
|
+
|
|
120
|
+
# Try to get default agent from environment variable (stored in gateway module)
|
|
121
|
+
default_agent_from_env = get_default_agent_id()
|
|
122
|
+
|
|
123
|
+
# Priority 1: GATEWAY_DEFAULT_AGENT environment variable
|
|
124
|
+
if default_agent_from_env:
|
|
125
|
+
# Validate that this agent exists in policy rules
|
|
126
|
+
if default_agent_from_env in self.policy_engine.agents:
|
|
127
|
+
return default_agent_from_env
|
|
128
|
+
else:
|
|
129
|
+
raise ToolError(
|
|
130
|
+
f"Missing 'agent_id' parameter and fallback agent '{default_agent_from_env}' "
|
|
131
|
+
f"is not configured in gateway rules.\n"
|
|
132
|
+
f"Either provide 'agent_id' in your tool calls, or ask the user to configure "
|
|
133
|
+
f"the gateway fallback settings. See gateway documentation for configuration options."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Priority 2: Agent named "default" in gateway rules
|
|
137
|
+
if "default" in self.policy_engine.agents:
|
|
138
|
+
return "default"
|
|
139
|
+
|
|
140
|
+
# Priority 3: No fallback configured - provide helpful error
|
|
141
|
+
raise ToolError(
|
|
142
|
+
"Missing 'agent_id' parameter and no fallback agent configured.\n"
|
|
143
|
+
"Either provide 'agent_id' in your tool calls, or ask the user to configure "
|
|
144
|
+
"the gateway fallback settings. See gateway documentation for configuration options."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async def on_list_tools(self, context: MiddlewareContext, call_next):
|
|
148
|
+
"""Pass through list_tools requests without filtering.
|
|
149
|
+
|
|
150
|
+
Gateway tools (list_servers, get_server_tools, execute_tool) should
|
|
151
|
+
always be visible to all agents since they perform their own
|
|
152
|
+
authorization based on agent_id passed in arguments.
|
|
153
|
+
|
|
154
|
+
This differs from a traditional MCP proxy that might filter downstream
|
|
155
|
+
tools at the middleware level. Our gateway exposes only 3 gateway tools
|
|
156
|
+
that act as an API for discovering and executing downstream tools.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
context: Middleware context containing the list request
|
|
160
|
+
call_next: Callable to invoke next middleware/handler in chain
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Full list of gateway tools from downstream handler
|
|
164
|
+
"""
|
|
165
|
+
# No filtering needed - gateway tools handle their own authorization
|
|
166
|
+
return await call_next(context)
|