|
| 1 | +# PRD: x-ms-agentId Header for MCP Platform Calls |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Add an `x-ms-agentid` header to all outbound HTTP requests from the tooling package to the MCP platform. This header identifies the calling agent using the best available identifier. |
| 6 | + |
| 7 | +## Problem Statement |
| 8 | + |
| 9 | +The MCP platform needs to identify which agent is making tooling requests for: |
| 10 | +- Logging and diagnostics |
| 11 | +- Usage analytics |
| 12 | + |
| 13 | +Currently, no consistent agent identifier is sent with MCP platform requests. |
| 14 | + |
| 15 | +## Requirements |
| 16 | + |
| 17 | +### Functional Requirements |
| 18 | + |
| 19 | +1. All HTTP requests to the MCP platform SHALL include the `x-ms-agentid` header |
| 20 | +2. The header value SHALL be determined using the following priority: |
| 21 | + 1. **Agent Blueprint ID** from TurnContext (highest priority) |
| 22 | + 2. **Token Claims** - `xms_par_app_azp` (agent blueprint ID) > `appid` > `azp` |
| 23 | + 3. **Application Name** from environment or pyproject.toml (lowest priority fallback) |
| 24 | +3. If no identifier is available, the header SHOULD be omitted (not sent with empty value) |
| 25 | + |
| 26 | +### Non-Functional Requirements |
| 27 | + |
| 28 | +1. No additional network calls to retrieve identifiers |
| 29 | +2. Minimal performance impact on existing flows |
| 30 | +3. Backward compatible - existing integrations continue to work |
| 31 | + |
| 32 | +## Technical Design |
| 33 | + |
| 34 | +### Affected Components |
| 35 | + |
| 36 | +| Package | File | Change | |
| 37 | +|---------|------|--------| |
| 38 | +| `microsoft-agents-a365-runtime` | `utility.py` | Add `get_agent_id_from_token()` (checks `xms_par_app_azp` → `appid` → `azp`) | |
| 39 | +| `microsoft-agents-a365-runtime` | `utility.py` | Add `get_application_name()` helper (reads package name) | |
| 40 | +| `microsoft-agents-a365-tooling` | `constants.py` | Add `HEADER_AGENT_ID` and `HEADER_CHANNEL_ID` constants | |
| 41 | +| `microsoft-agents-a365-tooling` | `mcp_tool_server_configuration_service.py` | Update `_prepare_gateway_headers()` to include `x-ms-agentid` | |
| 42 | + |
| 43 | +### Identifier Retrieval Strategy |
| 44 | + |
| 45 | +#### 1. Agent Blueprint ID (Highest Priority) |
| 46 | + |
| 47 | +**Source**: `turn_context.activity.from_.agentic_app_blueprint_id` |
| 48 | + |
| 49 | +**Availability**: Only available in agentic request scenarios where a `TurnContext` is present and the request originates from another agent. |
| 50 | + |
| 51 | +**Format**: GUID (e.g., `12345678-1234-1234-1234-123456789abc`) |
| 52 | + |
| 53 | +--- |
| 54 | + |
| 55 | +#### 2 & 3. Agent ID from Token (Second/Third Priority) |
| 56 | + |
| 57 | +**Sources** (checked in order): |
| 58 | +1. `xms_par_app_azp` claim - Agent Blueprint ID (parent application's Azure app ID) |
| 59 | +2. `appid` or `azp` claim - Entra Application ID |
| 60 | + |
| 61 | +**Availability**: Available when an `auth_token` is provided to the tooling methods. |
| 62 | + |
| 63 | +**Implementation**: |
| 64 | + |
| 65 | +```python |
| 66 | +# libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/utility.py |
| 67 | +@staticmethod |
| 68 | +def get_agent_id_from_token(token: Optional[str]) -> str: |
| 69 | + """ |
| 70 | + Decodes the token and retrieves the best available agent identifier. |
| 71 | + Checks claims in priority order: xms_par_app_azp > appid > azp. |
| 72 | + |
| 73 | + Returns empty string for empty/missing tokens (unlike get_app_id_from_token |
| 74 | + which returns a default GUID). |
| 75 | + """ |
| 76 | + if not token or not token.strip(): |
| 77 | + return "" |
| 78 | + |
| 79 | + try: |
| 80 | + decoded = jwt.decode(token, options={"verify_signature": False}) |
| 81 | + # Priority: xms_par_app_azp (agent blueprint ID) > appid > azp |
| 82 | + return decoded.get("xms_par_app_azp") or decoded.get("appid") or decoded.get("azp") or "" |
| 83 | + except (jwt.DecodeError, jwt.InvalidTokenError): |
| 84 | + return "" |
| 85 | +``` |
| 86 | + |
| 87 | +**Format**: GUID (e.g., `12345678-1234-1234-1234-123456789abc`) |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +#### 4. Application Name (Lowest Priority Fallback) |
| 92 | + |
| 93 | +**Source**: Application's pyproject.toml `name` field or environment variable |
| 94 | + |
| 95 | +**Strategy**: |
| 96 | +1. Check `AGENT365_APPLICATION_NAME` environment variable |
| 97 | +2. Fall back to reading and caching the application's `pyproject.toml` name field |
| 98 | +3. If neither available, omit the header |
| 99 | + |
| 100 | +**Implementation**: |
| 101 | + |
| 102 | +```python |
| 103 | +# libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/utility.py |
| 104 | +_cached_application_name: Optional[str] = None |
| 105 | + |
| 106 | +@staticmethod |
| 107 | +def get_application_name() -> Optional[str]: |
| 108 | + """Gets the application name from environment or pyproject.toml.""" |
| 109 | + # First try environment variable |
| 110 | + env_name = os.environ.get("AGENT365_APPLICATION_NAME") |
| 111 | + if env_name: |
| 112 | + return env_name |
| 113 | + |
| 114 | + # Fall back to cached pyproject.toml name |
| 115 | + if Utility._cached_application_name is None: |
| 116 | + Utility._cached_application_name = Utility._read_application_name() |
| 117 | + |
| 118 | + return Utility._cached_application_name or None |
| 119 | +``` |
| 120 | + |
| 121 | +--- |
| 122 | + |
| 123 | +### Implementation |
| 124 | + |
| 125 | +#### Updated Header Preparation |
| 126 | + |
| 127 | +```python |
| 128 | +# libraries/microsoft-agents-a365-tooling/.../mcp_tool_server_configuration_service.py |
| 129 | +def _prepare_gateway_headers( |
| 130 | + self, |
| 131 | + auth_token: str, |
| 132 | + turn_context: Optional[TurnContext], |
| 133 | + options: ToolOptions |
| 134 | +) -> Dict[str, str]: |
| 135 | + """Prepares headers for tooling gateway requests.""" |
| 136 | + headers = { |
| 137 | + Constants.Headers.AUTHORIZATION: f"{Constants.Headers.BEARER_PREFIX} {auth_token}", |
| 138 | + Constants.Headers.USER_AGENT: RuntimeUtility.get_user_agent_header( |
| 139 | + options.orchestrator_name |
| 140 | + ), |
| 141 | + } |
| 142 | + |
| 143 | + # Add x-ms-agentid header with priority fallback |
| 144 | + agent_id = self._resolve_agent_id_for_header(auth_token, turn_context) |
| 145 | + if agent_id: |
| 146 | + headers[Constants.Headers.AGENT_ID] = agent_id |
| 147 | + |
| 148 | + return headers |
| 149 | + |
| 150 | +def _resolve_agent_id_for_header( |
| 151 | + self, |
| 152 | + auth_token: str, |
| 153 | + turn_context: Optional[TurnContext] |
| 154 | +) -> Optional[str]: |
| 155 | + """Resolves the best available agent identifier for the x-ms-agentid header.""" |
| 156 | + # Priority 1: Agent Blueprint ID from TurnContext |
| 157 | + try: |
| 158 | + if turn_context and turn_context.activity and turn_context.activity.from_: |
| 159 | + blueprint_id = getattr(turn_context.activity.from_, 'agentic_app_blueprint_id', None) |
| 160 | + if blueprint_id: |
| 161 | + return blueprint_id |
| 162 | + except (AttributeError, TypeError): |
| 163 | + pass |
| 164 | + |
| 165 | + # Priority 2 & 3: Agent ID from token (xms_par_app_azp > appid > azp) |
| 166 | + agent_id = RuntimeUtility.get_agent_id_from_token(auth_token) |
| 167 | + if agent_id: |
| 168 | + return agent_id |
| 169 | + |
| 170 | + # Priority 4: Application name |
| 171 | + return RuntimeUtility.get_application_name() |
| 172 | +``` |
| 173 | + |
| 174 | +### Call Sites Summary |
| 175 | + |
| 176 | +| Call Site | auth_token | turn_context | Gets `x-ms-agentid`? | |
| 177 | +|-----------|-----------|-------------|----------------------| |
| 178 | +| `_load_servers_from_gateway()` | ✅ | ❌ (None currently) | ✅ Yes (from token/app name) | |
| 179 | +| `send_chat_history()` | ❌ | ✅ | ❌ No (authToken required) | |
| 180 | + |
| 181 | +**Note**: The `x-ms-agentid` header is only added when `auth_token` is present. `send_chat_history()` does not pass an auth token, so it won't include this header. |
| 182 | + |
| 183 | +--- |
| 184 | + |
| 185 | +## Open Questions |
| 186 | + |
| 187 | +### Q1: Application Name Strategy ✅ RESOLVED |
| 188 | + |
| 189 | +**Decision**: Use `AGENT365_APPLICATION_NAME` environment variable as primary, with pyproject.toml fallback. Cache the pyproject.toml read to avoid repeated file I/O. |
| 190 | + |
| 191 | +### Q2: Header Name Casing ✅ RESOLVED |
| 192 | + |
| 193 | +**Decision**: Use `x-ms-agentid` (all lowercase, case insensitive). |
| 194 | + |
| 195 | +HTTP headers are case-insensitive per RFC 7230, so the server will accept any casing. Using lowercase is the conventional choice. |
| 196 | + |
| 197 | +### Q3: TurnContext Availability ✅ RESOLVED |
| 198 | + |
| 199 | +**Decision**: For this initial implementation, we will pass `None` for turn_context in `_load_servers_from_gateway()` since the current `list_tool_servers` signature doesn't accept it. The agent ID will be resolved from the token or application name. |
| 200 | + |
| 201 | +A future enhancement can add a new overloaded signature similar to the Node.js SDK that accepts `TurnContext`. |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## Testing Strategy |
| 206 | + |
| 207 | +### Unit Tests |
| 208 | + |
| 209 | +1. Test `get_agent_id_from_token()` with each priority level: |
| 210 | + - Token with `xms_par_app_azp` → returns blueprint ID from token |
| 211 | + - No `xms_par_app_azp`, token with `appid` → returns Entra app ID |
| 212 | + - No `appid`, token with `azp` → returns azp claim |
| 213 | + - No token claims → returns empty string |
| 214 | + - Empty/invalid token → returns empty string |
| 215 | +2. Test `get_application_name()`: |
| 216 | + - Returns env var when set |
| 217 | + - Returns pyproject.toml name when env not set |
| 218 | + - Returns None when nothing available |
| 219 | + - Caches the result |
| 220 | +3. Test `_resolve_agent_id_for_header()`: |
| 221 | + - TurnContext with `agentic_app_blueprint_id` → returns blueprint ID |
| 222 | + - No blueprint ID, token with claims → returns token claim |
| 223 | + - No claims → returns application name |
| 224 | + - Nothing available → returns None |
| 225 | +4. Test `_prepare_gateway_headers()`: |
| 226 | + - Includes `x-ms-agentid` when identifier available |
| 227 | + - Omits header when no identifier available |
| 228 | + |
| 229 | +### Integration Tests |
| 230 | + |
| 231 | +1. Verify header is sent in `list_tool_servers()` requests |
| 232 | +2. Verify header is NOT sent in `send_chat_history()` requests (no authToken) |
| 233 | + |
| 234 | +--- |
| 235 | + |
| 236 | +## Breaking Changes |
| 237 | + |
| 238 | +**None** - This implementation is fully backward compatible. |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +## Rollout Plan |
| 243 | + |
| 244 | +1. **Phase 1**: Add utility methods and `x-ms-agentid` header (this PR) |
| 245 | +2. **Phase 2**: Add overloaded `list_tool_servers()` signature with TurnContext (future) |
| 246 | +3. **Phase 3**: Update documentation and samples |
| 247 | + |
| 248 | +--- |
| 249 | + |
| 250 | +## Dependencies |
| 251 | + |
| 252 | +- Runtime package for token utility (already exists) |
| 253 | +- PyJWT library (already a dependency) |
| 254 | +- No new external dependencies required |
| 255 | + |
| 256 | +--- |
| 257 | + |
| 258 | +## Success Metrics |
| 259 | + |
| 260 | +1. 100% of MCP platform requests include `x-ms-agentid` header (when identifier available) |
| 261 | +2. No increase in request latency |
| 262 | +3. No breaking changes for existing consumers |
0 commit comments