Skip to content

Commit 730bcee

Browse files
Python: Autolabelling MCP servers based on hints and Github MCP server ifc labels (microsoft#6171)
* Python: add GitHub MCP security label sample * modified samples to create devui auth token, support debugging with security, and change context label only using the labels of unhidden result from tools * FIDES: secure MCP labeling, _meta IFC parsing, and docs updates * FIDES: secure MCP labeling, _meta IFC parsing, and docs updates * modified docs * fixed PR comments, simplified github_mcp example * commented github_mcp example * remove the parse_github_mcp_labels and fix the user_identity label propogation * fix: use standard GitHub MCP endpoint with X-MCP-Features: ifc_labels instead of /insiders - Switch MCP_URL from /mcp/insiders to /mcp/ in github_mcp_example.py - Add MCP_HEADERS constant with X-MCP-Features: ifc_labels to opt-in to server-side IFC label emission in _meta payloads - Fix SecureMCPToolProxy to pass headers via httpx.AsyncClient so they are included on session.initialize(), not just on tool calls (was causing 401 to silently surface as anyio cancel-scope CancelledError) - Update README, FIDES_DEVELOPER_GUIDE, FIDES_IMPLEMENTATION_SUMMARY, and 0024-prompt-injection-defense.md to remove all /insiders references * address PR comments * Simplify GitHub MCP security sample to DevUI-only; document SecureAgentConfig quarantine client global behavior * minor PR comments * fixing failed checks * fixing failed checks --------- Co-authored-by: Eduard van Valkenburg <[email protected]>
1 parent a1c37b6 commit 730bcee

11 files changed

Lines changed: 2011 additions & 320 deletions

File tree

docs/decisions/0024-prompt-injection-defense.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ FIDES (Flow Integrity Deterministic Enforcement System) is a label-based securit
4343
3. **Variable Indirection**`ContentVariableStore` and `VariableReferenceContent` for physical isolation of untrusted content from the LLM context.
4444
4. **Quarantined Execution**`quarantined_llm` and `inspect_variable` tools for isolated processing of untrusted data with audit logging.
4545

46+
In addition, remote MCP integrations are secured through two mechanisms:
47+
48+
- **Hint-based tool auto-labeling**: MCP `ToolAnnotations` (`readOnlyHint`, `openWorldHint`, etc.) are mapped to FIDES tool properties (`source_integrity`, `accepts_untrusted`, `max_allowed_confidentiality`).
49+
- **Server `_meta.ifc` result labels**: MCP result metadata is parsed into per-item `security_label` values, so provider-supplied IFC labels are enforced by middleware.
50+
4651
### Consequences
4752

4853
- Good, because it provides deterministic security guarantees about what untrusted content can influence.
@@ -117,6 +122,13 @@ Monitor agent behavior and block suspicious actions post-facto.
117122
- Uses existing `FunctionMiddleware` base class.
118123
- Attaches labels via `additional_properties` (no schema changes).
119124
- Leverages `SerializationMixin` for label persistence.
125+
- Integrates MCP hint/result metadata through `additional_properties` keys (`max_allowed_confidentiality`, `source_integrity`, `__mcp_result_meta__`) without transport-specific policy code in core middleware.
126+
127+
### MCP-Specific Security Notes
128+
129+
- `SecureMCPToolProxy` applies `apply_mcp_security_labels(...)` automatically when connecting an MCP tool or URL.
130+
- For servers like the GitHub MCP server (with `X-MCP-Features: ifc_labels`), `_meta.ifc` labels are considered authoritative for per-result label assignment.
131+
- Tools that are not explicitly `readOnlyHint=True` are treated as potential sinks and default to `max_allowed_confidentiality=PUBLIC` to prevent exfiltration.
120132

121133

122134
### Backwards Compatibility

docs/features/FIDES_IMPLEMENTATION_SUMMARY.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
- **Context Provider Pattern** - `SecureAgentConfig` extends `ContextProvider`, injecting tools, instructions, and middleware automatically
99
- **Automatic Variable Hiding** - UNTRUSTED content is automatically hidden without requiring manual intervention
1010
- **Per-Item Embedded Labels** - Tools return `list[Content]` with `Content.from_text()` for proper label propagation
11+
- **SecureMCPToolProxy Auto-Labeling** - MCP tools are labeled automatically from MCP `ToolAnnotations` hints
12+
- **MCP `_meta.ifc` Support** - Per-result IFC labels from servers (for example GitHub MCP with `X-MCP-Features: ifc_labels`) are parsed and enforced
1113
- **SecureAgentConfig** - One-line secure agent configuration via `context_providers=[config]`
1214
- **Data Exfiltration Prevention** - `max_allowed_confidentiality` prevents sensitive data leakage
1315
- **Message-Level Label Tracking** (Phase 1) - Track labels on every message in the conversation
@@ -23,6 +25,7 @@ The FIDES defense system consists of seven main components:
2325
5. **Security Tools** - Specialized tools for safe handling of untrusted content (`quarantined_llm`, `inspect_variable`)
2426
6. **SecureAgentConfig** - Context provider for easy secure agent configuration
2527
7. **Message-Level Label Tracking** - Track labels on every message in the conversation (Phase 1)
28+
8. **MCP Tool/Result Label Integration** - MCP hint-based tool labeling and `_meta.ifc` result label parsing
2629

2730
## Implementation Details
2831

@@ -184,6 +187,17 @@ agent = Agent(
184187
)
185188
```
186189

190+
### 7. MCP Labeling Pipeline (Hints + `_meta.ifc`)
191+
192+
FIDES now secures remote MCP integration end-to-end:
193+
194+
- **Tool labels from hints**: `apply_mcp_security_labels(...)` maps MCP hints (`readOnlyHint`, `openWorldHint`) to FIDES tool properties.
195+
- **Safe sink defaults**: tools not explicitly marked `readOnlyHint=True` are treated as potential sinks and receive `max_allowed_confidentiality=public`.
196+
- **Result labels from metadata**: MCP result `_meta` is propagated via `__mcp_result_meta__`; `_meta.ifc` is parsed into `security_label` per result item.
197+
- **`SecureMCPToolProxy` convenience**: wraps MCP tools/URLs and applies this labeling automatically on connect.
198+
199+
This behavior is used with the GitHub MCP server when `X-MCP-Features: ifc_labels` is passed, which causes the server to return IFC labels in `_meta` (for example `{"ifc": {"integrity": "untrusted", "confidentiality": "public"}}`).
200+
187201
## Security Properties
188202

189203
### Deterministic Defense

python/packages/core/agent_framework/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
chat_middleware,
168168
function_middleware,
169169
)
170+
170171
from ._sessions import (
171172
AgentSession,
172173
ContextProvider,

python/packages/core/agent_framework/_mcp.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -585,33 +585,51 @@ def _parse_tool_result_from_mcp(
585585
self,
586586
mcp_type: types.CallToolResult,
587587
) -> list[Content]:
588-
"""Parse an MCP CallToolResult into a list of Content items."""
588+
"""Parse an MCP CallToolResult into a list of Content items.
589+
590+
If the server attached a ``_meta`` payload to the tool result (e.g. for
591+
Information Flow Control labels under the ``ifc`` key), a copy of that
592+
payload is stamped onto each produced :class:`Content` instance under
593+
``additional_properties["_meta"]``. Downstream layers (such as
594+
:class:`agent_framework.security.SecureMCPToolProxy`) consume this key
595+
to derive per-item security labels.
596+
The sentinel is intentionally generic so any MCP server's ``_meta``
597+
keys (current or future) can be interpreted by higher-level code.
598+
"""
589599
from mcp import types
590600

601+
raw_meta = mcp_type.meta
602+
meta: dict[str, Any] | None = dict(raw_meta) if isinstance(raw_meta, Mapping) else None
603+
# Stamp the server ``_meta`` payload directly via additional_properties on
604+
# each newly constructed Content; empty when the server provided no meta.
605+
additional_kwargs: dict[str, Any] = {"additional_properties": {"_meta": meta}} if meta else {}
606+
591607
result: list[Content] = []
592608
for item in mcp_type.content:
593609
match item:
594610
case types.TextContent():
595-
result.append(Content.from_text(item.text))
611+
result.append(Content.from_text(item.text, **additional_kwargs))
596612
case types.ImageContent() | types.AudioContent():
597613
decoded = base64.b64decode(item.data)
598614
result.append(
599615
Content.from_data(
600616
data=decoded,
601617
media_type=item.mimeType,
618+
**additional_kwargs,
602619
)
603620
)
604621
case types.ResourceLink():
605622
result.append(
606623
Content.from_uri(
607624
uri=str(item.uri),
608625
media_type=item.mimeType,
626+
**additional_kwargs,
609627
)
610628
)
611629
case types.EmbeddedResource():
612630
match item.resource:
613631
case types.TextResourceContents():
614-
result.append(Content.from_text(item.resource.text))
632+
result.append(Content.from_text(item.resource.text, **additional_kwargs))
615633
case types.BlobResourceContents():
616634
blob = item.resource.blob
617635
mime = item.resource.mimeType or "application/octet-stream"
@@ -621,16 +639,17 @@ def _parse_tool_result_from_mcp(
621639
Content.from_uri(
622640
uri=blob,
623641
media_type=mime,
642+
**additional_kwargs,
624643
)
625644
)
626645
case _:
627-
result.append(Content.from_text(str(item)))
646+
result.append(Content.from_text(str(item), **additional_kwargs))
628647

629648
if mcp_type.structuredContent is not None:
630649
result.append(Content.from_text(json.dumps(mcp_type.structuredContent, default=str)))
631650

632651
if not result:
633-
result.append(Content.from_text("null"))
652+
result.append(Content.from_text("null", **additional_kwargs))
634653
return result
635654

636655
def _parse_content_from_mcp(

0 commit comments

Comments
 (0)